@a-company/paradigm 1.5.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 +142 -0
- package/dist/accept-orchestration-CWZNCGZX.js +188 -0
- package/dist/agents-suggest-35LIQKDH.js +83 -0
- package/dist/aggregate-W7Q6VIM2.js +88 -0
- package/dist/auto-IU7VN55K.js +470 -0
- package/dist/beacon-B47XSTL7.js +251 -0
- package/dist/chunk-2M6OSOIG.js +1302 -0
- package/dist/chunk-4NCFWYGG.js +110 -0
- package/dist/chunk-5C4SGQKH.js +705 -0
- package/dist/chunk-5GOA7WYD.js +1095 -0
- package/dist/chunk-5JGJACDU.js +37 -0
- package/dist/chunk-6QC3YGB6.js +114 -0
- package/dist/chunk-753RICFF.js +325 -0
- package/dist/chunk-AD2LSCHB.js +1595 -0
- package/dist/chunk-CHSHON3O.js +669 -0
- package/dist/chunk-ELLR7WP6.js +3175 -0
- package/dist/chunk-ILOWBJRC.js +12 -0
- package/dist/chunk-IRKUEJVW.js +405 -0
- package/dist/chunk-MC7XC7XQ.js +533 -0
- package/dist/chunk-MO4EEYFW.js +38 -0
- package/dist/chunk-MQWH7PFI.js +13366 -0
- package/dist/chunk-N6PJAPDE.js +364 -0
- package/dist/chunk-PBHIFAL4.js +259 -0
- package/dist/chunk-PMXRGPRQ.js +305 -0
- package/dist/chunk-PW2EXJQT.js +689 -0
- package/dist/chunk-TAP5N3HH.js +245 -0
- package/dist/chunk-THFVK5AE.js +148 -0
- package/dist/chunk-UM54F7G5.js +1533 -0
- package/dist/chunk-UUZ2DMG5.js +185 -0
- package/dist/chunk-WS5KM7OL.js +780 -0
- package/dist/chunk-YDNKXH4Z.js +2316 -0
- package/dist/chunk-YO6DVTL7.js +99 -0
- package/dist/claude-SUYNN72C.js +362 -0
- package/dist/claude-cli-OF43XAO3.js +276 -0
- package/dist/claude-code-PW6SKD2M.js +126 -0
- package/dist/claude-code-teams-JLZ5IXB6.js +199 -0
- package/dist/constellation-K3CIQCHI.js +225 -0
- package/dist/cost-AEK6R7HK.js +174 -0
- package/dist/cost-KYXIQ62X.js +93 -0
- package/dist/cursor-cli-IHJMPRCW.js +269 -0
- package/dist/cursorrules-KI5QWHIX.js +84 -0
- package/dist/diff-AJJ5H6HV.js +125 -0
- package/dist/dist-7MPIRMTZ-IOQOREMZ.js +10866 -0
- package/dist/dist-NHJQVVUW.js +68 -0
- package/dist/dist-ZEMSQV74.js +20 -0
- package/dist/doctor-6Y6L6HEB.js +11 -0
- package/dist/echo-VYZW3OTT.js +248 -0
- package/dist/export-R4FJ5NOH.js +38 -0
- package/dist/history-EVO3L6SC.js +277 -0
- package/dist/hooks-MBWE4ILT.js +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +568 -0
- package/dist/lint-HXKTWRNO.js +316 -0
- package/dist/manual-Y3QOXWYA.js +204 -0
- package/dist/mcp.js +14745 -0
- package/dist/orchestrate-4ZH5GUQH.js +323 -0
- package/dist/probe-OYCP4JYG.js +151 -0
- package/dist/promote-Z52ZJTJU.js +181 -0
- package/dist/providers-4PGPZEWP.js +104 -0
- package/dist/remember-6VZ74B7E.js +77 -0
- package/dist/ripple-SBQOSTZD.js +215 -0
- package/dist/sentinel-LCFD56OJ.js +43 -0
- package/dist/server-F5ITNK6T.js +9846 -0
- package/dist/server-T6WIFYRQ.js +16076 -0
- package/dist/setup-DF4F3ICN.js +25 -0
- package/dist/setup-JHBPZAG7.js +296 -0
- package/dist/shift-HKIAP4ZN.js +226 -0
- package/dist/snapshot-GTVPRYZG.js +62 -0
- package/dist/spawn-BJRQA2NR.js +196 -0
- package/dist/summary-H6J6N6PJ.js +140 -0
- package/dist/switch-6EANJ7O6.js +232 -0
- package/dist/sync-BEOCW7TZ.js +11 -0
- package/dist/team-NWP2KJAB.js +32 -0
- package/dist/test-MA5TWJQV.js +934 -0
- package/dist/thread-JCJVRUQR.js +258 -0
- package/dist/triage-ETVXXFMV.js +1880 -0
- package/dist/tutorial-L5Q3ZDHK.js +666 -0
- package/dist/university-R2WDQLSI.js +40 -0
- package/dist/upgrade-5B3YGGC6.js +550 -0
- package/dist/validate-F3YHBCRZ.js +39 -0
- package/dist/validate-QEEY6KFS.js +64 -0
- package/dist/watch-4LT4O6K7.js +123 -0
- package/dist/watch-6IIWPWDN.js +111 -0
- package/dist/wisdom-LRM4FFCH.js +319 -0
- package/package.json +68 -0
- package/templates/paradigm/config.yaml +175 -0
- package/templates/paradigm/docs/commands.md +727 -0
- package/templates/paradigm/docs/decisions/000-template.md +47 -0
- package/templates/paradigm/docs/decisions/README.md +26 -0
- package/templates/paradigm/docs/error-patterns.md +215 -0
- package/templates/paradigm/docs/patterns.md +358 -0
- package/templates/paradigm/docs/queries.md +200 -0
- package/templates/paradigm/docs/troubleshooting.md +477 -0
- package/templates/paradigm/echoes.yaml +25 -0
- package/templates/paradigm/prompts/add-feature.md +152 -0
- package/templates/paradigm/prompts/add-gate.md +117 -0
- package/templates/paradigm/prompts/debug-auth.md +174 -0
- package/templates/paradigm/prompts/implement-ftux.md +722 -0
- package/templates/paradigm/prompts/implement-sandbox.md +651 -0
- package/templates/paradigm/prompts/read-docs.md +84 -0
- package/templates/paradigm/prompts/refactor.md +106 -0
- package/templates/paradigm/prompts/run-e2e-tests.md +340 -0
- package/templates/paradigm/prompts/trace-flow.md +202 -0
- package/templates/paradigm/prompts/validate-portals.md +279 -0
- package/templates/paradigm/specs/context-tracking.md +200 -0
- package/templates/paradigm/specs/context.md +461 -0
- package/templates/paradigm/specs/disciplines.md +413 -0
- package/templates/paradigm/specs/history.md +339 -0
- package/templates/paradigm/specs/logger.md +303 -0
- package/templates/paradigm/specs/navigator.md +236 -0
- package/templates/paradigm/specs/purpose.md +265 -0
- package/templates/paradigm/specs/scan.md +177 -0
- package/templates/paradigm/specs/symbols.md +451 -0
- package/templates/paradigm/specs/wisdom.md +294 -0
|
@@ -0,0 +1,1302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/hooks/index.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
var POST_COMMIT_HOOK = `#!/bin/sh
|
|
8
|
+
# Paradigm post-commit hook - captures history from commits
|
|
9
|
+
# Installed by: paradigm hooks install
|
|
10
|
+
|
|
11
|
+
# Get the commit message and hash
|
|
12
|
+
COMMIT_HASH=$(git rev-parse HEAD)
|
|
13
|
+
COMMIT_MSG=$(git log -1 --pretty=%B)
|
|
14
|
+
|
|
15
|
+
# Get changed files
|
|
16
|
+
CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD)
|
|
17
|
+
|
|
18
|
+
# Extract symbols from changed files (look for .purpose files)
|
|
19
|
+
extract_symbols() {
|
|
20
|
+
local symbols=""
|
|
21
|
+
for file in $CHANGED_FILES; do
|
|
22
|
+
# Check if there's a .purpose file in the directory
|
|
23
|
+
dir=$(dirname "$file")
|
|
24
|
+
while [ "$dir" != "." ]; do
|
|
25
|
+
if [ -f "$dir/.purpose" ]; then
|
|
26
|
+
# Extract feature/component names from .purpose
|
|
27
|
+
purpose_symbols=$(grep -E '^(features|components|gates|flows):' "$dir/.purpose" -A 10 2>/dev/null | grep -E '^ - (name|id):' | sed 's/.*: //' | tr '\\n' ',' | sed 's/,$//')
|
|
28
|
+
if [ -n "$purpose_symbols" ]; then
|
|
29
|
+
symbols="$symbols,$purpose_symbols"
|
|
30
|
+
fi
|
|
31
|
+
break
|
|
32
|
+
fi
|
|
33
|
+
dir=$(dirname "$dir")
|
|
34
|
+
done
|
|
35
|
+
done
|
|
36
|
+
echo "$symbols" | sed 's/^,//' | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
SYMBOLS=$(extract_symbols)
|
|
40
|
+
|
|
41
|
+
# Extract symbols from commit message Symbols: trailer
|
|
42
|
+
MSG_SYMBOLS=$(echo "$COMMIT_MSG" | grep -E '^Symbols:' | sed 's/^Symbols: //' | tr -d ' ')
|
|
43
|
+
if [ -n "$MSG_SYMBOLS" ]; then
|
|
44
|
+
if [ -n "$SYMBOLS" ]; then
|
|
45
|
+
SYMBOLS="$SYMBOLS,$MSG_SYMBOLS"
|
|
46
|
+
else
|
|
47
|
+
SYMBOLS="$MSG_SYMBOLS"
|
|
48
|
+
fi
|
|
49
|
+
# Deduplicate
|
|
50
|
+
SYMBOLS=$(echo "$SYMBOLS" | tr ',' '\\n' | sort -u | tr '\\n' ',' | sed 's/,$//')
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Determine intent from commit message
|
|
54
|
+
determine_intent() {
|
|
55
|
+
case "$COMMIT_MSG" in
|
|
56
|
+
feat*|feature*|add*) echo "feature" ;;
|
|
57
|
+
fix*|bug*) echo "fix" ;;
|
|
58
|
+
refactor*) echo "refactor" ;;
|
|
59
|
+
*) echo "feature" ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
INTENT=$(determine_intent)
|
|
64
|
+
|
|
65
|
+
# Record if we found symbols (from .purpose or commit message) and .paradigm/history exists
|
|
66
|
+
if [ -n "$SYMBOLS" ] && [ -d ".paradigm/history" ]; then
|
|
67
|
+
# Generate entry ID
|
|
68
|
+
if [ -f ".paradigm/history/log.jsonl" ]; then
|
|
69
|
+
COUNT=$(wc -l < ".paradigm/history/log.jsonl" | tr -d ' ')
|
|
70
|
+
COUNT=$((COUNT + 1))
|
|
71
|
+
else
|
|
72
|
+
COUNT=1
|
|
73
|
+
fi
|
|
74
|
+
ID=$(printf "h%04d" $COUNT)
|
|
75
|
+
|
|
76
|
+
# Create entry
|
|
77
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
78
|
+
AUTHOR=$(git config user.name || echo "unknown")
|
|
79
|
+
|
|
80
|
+
# Format symbols as JSON array
|
|
81
|
+
SYMBOLS_JSON=$(echo "$SYMBOLS" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
|
|
82
|
+
|
|
83
|
+
# Format files as JSON array
|
|
84
|
+
FILES_JSON=$(echo "$CHANGED_FILES" | tr '\\n' ',' | sed 's/,$//' | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')
|
|
85
|
+
|
|
86
|
+
# Write entry
|
|
87
|
+
echo "{\\"id\\":\\"$ID\\",\\"ts\\":\\"$TIMESTAMP\\",\\"type\\":\\"implement\\",\\"symbols\\":[$SYMBOLS_JSON],\\"author\\":{\\"type\\":\\"human\\",\\"id\\":\\"$AUTHOR\\"},\\"commit\\":\\"$COMMIT_HASH\\",\\"intent\\":\\"$INTENT\\",\\"files\\":[$FILES_JSON],\\"description\\":\\"$(echo "$COMMIT_MSG" | head -1 | sed 's/"/\\\\"/g')\\"}" >> .paradigm/history/log.jsonl
|
|
88
|
+
|
|
89
|
+
echo "[paradigm] History entry $ID recorded"
|
|
90
|
+
fi
|
|
91
|
+
`;
|
|
92
|
+
var PRE_PUSH_HOOK = `#!/bin/sh
|
|
93
|
+
# Paradigm pre-push hook - reindex history before pushing
|
|
94
|
+
# Installed by: paradigm hooks install
|
|
95
|
+
|
|
96
|
+
if [ -d ".paradigm/history" ] && [ -f ".paradigm/history/log.jsonl" ]; then
|
|
97
|
+
echo "[paradigm] Reindexing history..."
|
|
98
|
+
npx paradigm history reindex 2>/dev/null || true
|
|
99
|
+
fi
|
|
100
|
+
`;
|
|
101
|
+
var CLAUDE_CODE_STOP_HOOK = `#!/bin/sh
|
|
102
|
+
# Paradigm Claude Code Stop Hook (v2)
|
|
103
|
+
# Validates paradigm compliance before allowing the agent to finish.
|
|
104
|
+
# Installed by: paradigm hooks install --claude-code
|
|
105
|
+
#
|
|
106
|
+
# Hook type: Stop
|
|
107
|
+
# Exit 0 = allow, Exit 2 = block with message
|
|
108
|
+
#
|
|
109
|
+
# Checks:
|
|
110
|
+
# 1. Source files modified without .purpose updates (threshold: 2+)
|
|
111
|
+
# 2. Modified source directories missing .purpose files entirely
|
|
112
|
+
# 3. Route-like patterns added without portal.yaml updates
|
|
113
|
+
# 4. Aspect anchor files that no longer exist
|
|
114
|
+
# 5. Per-directory .purpose freshness (tracked via .pending-review)
|
|
115
|
+
# 6. Aspect coverage advisory
|
|
116
|
+
|
|
117
|
+
# Read JSON from stdin (hook input)
|
|
118
|
+
INPUT=$(cat)
|
|
119
|
+
|
|
120
|
+
# Extract cwd from input (try jq first, fallback to grep)
|
|
121
|
+
if command -v jq >/dev/null 2>&1; then
|
|
122
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
123
|
+
else
|
|
124
|
+
CWD=$(echo "$INPUT" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [ -z "$CWD" ]; then
|
|
128
|
+
CWD="$(pwd)"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Not a paradigm project \u2014 pass
|
|
132
|
+
if [ ! -d "$CWD/.paradigm" ]; then
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
cd "$CWD" || exit 0
|
|
137
|
+
|
|
138
|
+
# Get modified files (uncommitted changes)
|
|
139
|
+
MODIFIED=$(git diff --name-only HEAD 2>/dev/null)
|
|
140
|
+
if [ -z "$MODIFIED" ]; then
|
|
141
|
+
# Clean up pending-review on pass
|
|
142
|
+
rm -f ".paradigm/.pending-review"
|
|
143
|
+
exit 0
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
VIOLATIONS=""
|
|
147
|
+
VIOLATION_COUNT=0
|
|
148
|
+
|
|
149
|
+
# --- Check 1: Source files modified without .purpose updates ---
|
|
150
|
+
SOURCE_COUNT=0
|
|
151
|
+
PARADIGM_COUNT=0
|
|
152
|
+
|
|
153
|
+
for file in $MODIFIED; do
|
|
154
|
+
case "$file" in
|
|
155
|
+
.paradigm/*|*.purpose|portal.yaml)
|
|
156
|
+
PARADIGM_COUNT=$((PARADIGM_COUNT + 1))
|
|
157
|
+
;;
|
|
158
|
+
*.md|*.lock|*.log|.gitignore|.env*|*.json) ;;
|
|
159
|
+
*)
|
|
160
|
+
SOURCE_COUNT=$((SOURCE_COUNT + 1))
|
|
161
|
+
;;
|
|
162
|
+
esac
|
|
163
|
+
done
|
|
164
|
+
|
|
165
|
+
if [ "$SOURCE_COUNT" -gt 1 ] && [ "$PARADIGM_COUNT" -eq 0 ]; then
|
|
166
|
+
VIOLATIONS="$VIOLATIONS
|
|
167
|
+
- You modified $SOURCE_COUNT source files but 0 paradigm files (.purpose/portal.yaml).
|
|
168
|
+
Update the nearest .purpose file for each modified code area."
|
|
169
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# --- Check 2: Modified source directories missing .purpose files ---
|
|
173
|
+
DIRS_WITHOUT_PURPOSE=""
|
|
174
|
+
|
|
175
|
+
for file in $MODIFIED; do
|
|
176
|
+
case "$file" in
|
|
177
|
+
.paradigm/*|*.md|*.lock|*.log|.gitignore|.env*|*.json|*.purpose|portal.yaml) continue ;;
|
|
178
|
+
esac
|
|
179
|
+
|
|
180
|
+
dir=$(dirname "$file")
|
|
181
|
+
# Walk up to find a .purpose file
|
|
182
|
+
found_purpose=false
|
|
183
|
+
check_dir="$dir"
|
|
184
|
+
while [ "$check_dir" != "." ] && [ "$check_dir" != "" ]; do
|
|
185
|
+
if [ -f "$check_dir/.purpose" ]; then
|
|
186
|
+
found_purpose=true
|
|
187
|
+
break
|
|
188
|
+
fi
|
|
189
|
+
check_dir=$(dirname "$check_dir")
|
|
190
|
+
done
|
|
191
|
+
# Also check root
|
|
192
|
+
if [ "$found_purpose" = false ] && [ -f ".purpose" ]; then
|
|
193
|
+
found_purpose=true
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
if [ "$found_purpose" = false ]; then
|
|
197
|
+
# Deduplicate directory names
|
|
198
|
+
case "$DIRS_WITHOUT_PURPOSE" in
|
|
199
|
+
*"$dir"*) ;;
|
|
200
|
+
*) DIRS_WITHOUT_PURPOSE="$DIRS_WITHOUT_PURPOSE $dir" ;;
|
|
201
|
+
esac
|
|
202
|
+
fi
|
|
203
|
+
done
|
|
204
|
+
|
|
205
|
+
if [ -n "$DIRS_WITHOUT_PURPOSE" ]; then
|
|
206
|
+
VIOLATIONS="$VIOLATIONS
|
|
207
|
+
- These directories have modified source files but no .purpose file anywhere in their path:
|
|
208
|
+
$DIRS_WITHOUT_PURPOSE
|
|
209
|
+
Create a .purpose file using paradigm_purpose_init + paradigm_purpose_add_component."
|
|
210
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# --- Check 3: Route patterns added without portal.yaml ---
|
|
214
|
+
if [ -f "portal.yaml" ] || echo "$MODIFIED" | grep -q "portal.yaml"; then
|
|
215
|
+
: # portal.yaml exists or was modified \u2014 OK
|
|
216
|
+
else
|
|
217
|
+
# Check if any modified files contain route-like patterns
|
|
218
|
+
ROUTE_FILES=""
|
|
219
|
+
for file in $MODIFIED; do
|
|
220
|
+
case "$file" in
|
|
221
|
+
*.ts|*.js|*.tsx|*.jsx|*.py|*.rs|*.go)
|
|
222
|
+
if [ -f "$file" ]; then
|
|
223
|
+
if grep -qE '\\.(get|post|put|patch|delete)\\s*\\(|router\\.|app\\.(get|post|put|delete)|@(Get|Post|Put|Delete)|#\\[actix_web::(get|post)' "$file" 2>/dev/null; then
|
|
224
|
+
ROUTE_FILES="$ROUTE_FILES $file"
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
227
|
+
;;
|
|
228
|
+
esac
|
|
229
|
+
done
|
|
230
|
+
|
|
231
|
+
if [ -n "$ROUTE_FILES" ]; then
|
|
232
|
+
VIOLATIONS="$VIOLATIONS
|
|
233
|
+
- Route/endpoint patterns found in modified files but no portal.yaml exists:
|
|
234
|
+
$ROUTE_FILES
|
|
235
|
+
Create portal.yaml with gate definitions. Use paradigm_gates_for_route for suggestions."
|
|
236
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# --- Check 4: Aspect anchor files that no longer exist ---
|
|
241
|
+
for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null); do
|
|
242
|
+
if grep -q "anchors:" "$purpose_file" 2>/dev/null; then
|
|
243
|
+
purpose_dir=$(dirname "$purpose_file")
|
|
244
|
+
in_anchors=false
|
|
245
|
+
while IFS= read -r line; do
|
|
246
|
+
case "$line" in
|
|
247
|
+
*"anchors:"*) in_anchors=true; continue ;;
|
|
248
|
+
*"- "*)
|
|
249
|
+
if [ "$in_anchors" = true ]; then
|
|
250
|
+
anchor_path=$(echo "$line" | sed 's/.*- //' | sed 's/:.*//' | tr -d ' ')
|
|
251
|
+
if [ -n "$anchor_path" ]; then
|
|
252
|
+
resolved_path="$purpose_dir/$anchor_path"
|
|
253
|
+
if [ ! -f "$resolved_path" ]; then
|
|
254
|
+
VIOLATIONS="$VIOLATIONS
|
|
255
|
+
- Aspect anchor '$anchor_path' in $purpose_file does not exist.
|
|
256
|
+
Update the anchor or remove the stale aspect."
|
|
257
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
;;
|
|
262
|
+
*) in_anchors=false ;;
|
|
263
|
+
esac
|
|
264
|
+
done < "$purpose_file"
|
|
265
|
+
fi
|
|
266
|
+
done
|
|
267
|
+
|
|
268
|
+
# --- Check 5: Per-directory .purpose freshness ---
|
|
269
|
+
PENDING_FILE=".paradigm/.pending-review"
|
|
270
|
+
if [ -f "$PENDING_FILE" ]; then
|
|
271
|
+
STALE_PURPOSES=""
|
|
272
|
+
while IFS= read -r tracked_file; do
|
|
273
|
+
[ -z "$tracked_file" ] && continue
|
|
274
|
+
# Find covering .purpose for this tracked file
|
|
275
|
+
check_dir=$(dirname "$tracked_file")
|
|
276
|
+
covering_purpose=""
|
|
277
|
+
while [ "$check_dir" != "." ] && [ "$check_dir" != "" ]; do
|
|
278
|
+
if [ -f "$check_dir/.purpose" ]; then
|
|
279
|
+
covering_purpose="$check_dir/.purpose"
|
|
280
|
+
break
|
|
281
|
+
fi
|
|
282
|
+
check_dir=$(dirname "$check_dir")
|
|
283
|
+
done
|
|
284
|
+
if [ -z "$covering_purpose" ] && [ -f ".purpose" ]; then
|
|
285
|
+
covering_purpose=".purpose"
|
|
286
|
+
fi
|
|
287
|
+
# Check if covering .purpose was also modified
|
|
288
|
+
if [ -n "$covering_purpose" ]; then
|
|
289
|
+
if ! echo "$MODIFIED" | grep -qxF "$covering_purpose"; then
|
|
290
|
+
# Deduplicate
|
|
291
|
+
case "$STALE_PURPOSES" in
|
|
292
|
+
*"$covering_purpose"*) ;;
|
|
293
|
+
*) STALE_PURPOSES="$STALE_PURPOSES $covering_purpose" ;;
|
|
294
|
+
esac
|
|
295
|
+
fi
|
|
296
|
+
fi
|
|
297
|
+
done < "$PENDING_FILE"
|
|
298
|
+
|
|
299
|
+
if [ -n "$STALE_PURPOSES" ]; then
|
|
300
|
+
VIOLATIONS="$VIOLATIONS
|
|
301
|
+
- These .purpose files cover modified source code but were NOT updated:
|
|
302
|
+
$STALE_PURPOSES
|
|
303
|
+
Update each with: #components, ~aspects (with anchors), !signals, \\$flows, ^gates."
|
|
304
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
305
|
+
fi
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# --- Check 6: Aspect coverage advisory ---
|
|
309
|
+
ADVISORY=""
|
|
310
|
+
HAS_ASPECTS=false
|
|
311
|
+
for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null); do
|
|
312
|
+
if grep -qE '^\\s*~' "$purpose_file" 2>/dev/null; then
|
|
313
|
+
HAS_ASPECTS=true
|
|
314
|
+
break
|
|
315
|
+
fi
|
|
316
|
+
done
|
|
317
|
+
|
|
318
|
+
if [ "$HAS_ASPECTS" = true ] && [ "$SOURCE_COUNT" -gt 0 ]; then
|
|
319
|
+
ASPECT_UPDATED=false
|
|
320
|
+
for file in $MODIFIED; do
|
|
321
|
+
case "$file" in
|
|
322
|
+
*.purpose)
|
|
323
|
+
if grep -qE '^\\s*~|anchors:|applies-to:' "$file" 2>/dev/null; then
|
|
324
|
+
ASPECT_UPDATED=true
|
|
325
|
+
break
|
|
326
|
+
fi
|
|
327
|
+
;;
|
|
328
|
+
esac
|
|
329
|
+
done
|
|
330
|
+
|
|
331
|
+
if [ "$ASPECT_UPDATED" = false ]; then
|
|
332
|
+
ADVISORY=" This project defines ~aspects with code anchors. Check if existing
|
|
333
|
+
~aspects need updated anchors or applies-to patterns."
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# --- Final verdict ---
|
|
338
|
+
if [ "$VIOLATION_COUNT" -gt 0 ]; then
|
|
339
|
+
echo "" >&2
|
|
340
|
+
echo "Paradigm compliance check failed ($VIOLATION_COUNT violation(s)):" >&2
|
|
341
|
+
echo "$VIOLATIONS" >&2
|
|
342
|
+
if [ -n "$ADVISORY" ]; then
|
|
343
|
+
echo "" >&2
|
|
344
|
+
echo "Advisory:" >&2
|
|
345
|
+
echo "$ADVISORY" >&2
|
|
346
|
+
fi
|
|
347
|
+
echo "" >&2
|
|
348
|
+
echo "Run these MCP tools to fix:" >&2
|
|
349
|
+
echo " 1. paradigm_purpose_add_component \u2014 register new code units" >&2
|
|
350
|
+
echo " 2. paradigm_purpose_add_aspect \u2014 register cross-cutting concerns (with anchors)" >&2
|
|
351
|
+
echo " 3. paradigm_portal_add_route \u2014 register new endpoints with gates" >&2
|
|
352
|
+
echo " 4. paradigm_reindex \u2014 rebuild indexes after updates" >&2
|
|
353
|
+
exit 2
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# Print advisory even on pass (informational)
|
|
357
|
+
if [ -n "$ADVISORY" ]; then
|
|
358
|
+
echo "" >&2
|
|
359
|
+
echo "[paradigm] Advisory:" >&2
|
|
360
|
+
echo "$ADVISORY" >&2
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# Clean up pending-review on pass
|
|
364
|
+
rm -f ".paradigm/.pending-review"
|
|
365
|
+
|
|
366
|
+
exit 0
|
|
367
|
+
`;
|
|
368
|
+
var CLAUDE_CODE_POSTWRITE_HOOK = `#!/bin/sh
|
|
369
|
+
# Paradigm Claude Code PostToolUse Hook (v2)
|
|
370
|
+
# Fires after Edit/Write tool calls.
|
|
371
|
+
# Tracks modified source files in .paradigm/.pending-review
|
|
372
|
+
# and outputs compliance reminders.
|
|
373
|
+
# Installed by: paradigm hooks install --claude-code
|
|
374
|
+
#
|
|
375
|
+
# Hook type: PostToolUse (matcher: Edit,Write)
|
|
376
|
+
# Exit 0 always (never blocks \u2014 advisory only)
|
|
377
|
+
|
|
378
|
+
# Read JSON from stdin (hook input)
|
|
379
|
+
INPUT=$(cat)
|
|
380
|
+
|
|
381
|
+
# Extract the file path from tool_input
|
|
382
|
+
if command -v jq >/dev/null 2>&1; then
|
|
383
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
|
|
384
|
+
else
|
|
385
|
+
FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
386
|
+
if [ -z "$FILE_PATH" ]; then
|
|
387
|
+
FILE_PATH=$(echo "$INPUT" | grep -o '"filePath"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"filePath"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
388
|
+
fi
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
if [ -z "$FILE_PATH" ]; then
|
|
392
|
+
exit 0
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
# Skip non-source files
|
|
396
|
+
case "$FILE_PATH" in
|
|
397
|
+
*.purpose|portal.yaml|*.md|*.lock|*.log|*.json|*.yaml|*.yml|.gitignore|.env*) exit 0 ;;
|
|
398
|
+
esac
|
|
399
|
+
|
|
400
|
+
# Skip .paradigm, .claude, and .cursor directories
|
|
401
|
+
case "$FILE_PATH" in
|
|
402
|
+
*/.paradigm/*|.paradigm/*|*/.claude/*|.claude/*|*/.cursor/*|.cursor/*) exit 0 ;;
|
|
403
|
+
esac
|
|
404
|
+
|
|
405
|
+
# Not a paradigm project \u2014 pass
|
|
406
|
+
if [ ! -d ".paradigm" ]; then
|
|
407
|
+
exit 0
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# Convert to relative path (strip project root prefix)
|
|
411
|
+
PROJECT_ROOT="$(pwd)"
|
|
412
|
+
REL_PATH="$FILE_PATH"
|
|
413
|
+
case "$FILE_PATH" in
|
|
414
|
+
"$PROJECT_ROOT"/*) REL_PATH=$(echo "$FILE_PATH" | sed "s|^$PROJECT_ROOT/||") ;;
|
|
415
|
+
esac
|
|
416
|
+
|
|
417
|
+
# If still absolute, file is outside project \u2014 skip
|
|
418
|
+
case "$REL_PATH" in
|
|
419
|
+
/*) exit 0 ;;
|
|
420
|
+
esac
|
|
421
|
+
|
|
422
|
+
# Track: append to .paradigm/.pending-review (deduplicated)
|
|
423
|
+
PENDING_FILE=".paradigm/.pending-review"
|
|
424
|
+
if [ -f "$PENDING_FILE" ]; then
|
|
425
|
+
if ! grep -qxF "$REL_PATH" "$PENDING_FILE" 2>/dev/null; then
|
|
426
|
+
echo "$REL_PATH" >> "$PENDING_FILE"
|
|
427
|
+
fi
|
|
428
|
+
else
|
|
429
|
+
echo "$REL_PATH" > "$PENDING_FILE"
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
# Count pending files
|
|
433
|
+
PENDING_COUNT=$(wc -l < "$PENDING_FILE" | tr -d ' ')
|
|
434
|
+
|
|
435
|
+
# Walk up from the file's directory to find a .purpose file
|
|
436
|
+
dir=$(dirname "$REL_PATH")
|
|
437
|
+
found_purpose=""
|
|
438
|
+
|
|
439
|
+
while [ "$dir" != "." ] && [ "$dir" != "/" ] && [ "$dir" != "" ]; do
|
|
440
|
+
if [ -f "$dir/.purpose" ]; then
|
|
441
|
+
found_purpose="$dir/.purpose"
|
|
442
|
+
break
|
|
443
|
+
fi
|
|
444
|
+
dir=$(dirname "$dir")
|
|
445
|
+
done
|
|
446
|
+
|
|
447
|
+
# Check root .purpose
|
|
448
|
+
if [ -z "$found_purpose" ] && [ -f ".purpose" ]; then
|
|
449
|
+
found_purpose=".purpose"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
if [ -z "$found_purpose" ]; then
|
|
453
|
+
file_dir=$(dirname "$REL_PATH")
|
|
454
|
+
echo "" >&2
|
|
455
|
+
echo "[paradigm] No .purpose file covers $file_dir/" >&2
|
|
456
|
+
echo " Create one: paradigm_purpose_init + paradigm_purpose_add_component" >&2
|
|
457
|
+
echo " $PENDING_COUNT file(s) pending review. The stop hook WILL BLOCK." >&2
|
|
458
|
+
elif [ "$PENDING_COUNT" -gt 0 ] && [ "$((PENDING_COUNT % 3))" -eq 0 ]; then
|
|
459
|
+
echo "" >&2
|
|
460
|
+
echo "[paradigm] $PENDING_COUNT source file(s) modified. Update $found_purpose:" >&2
|
|
461
|
+
echo " -> #components, ~aspects (with anchors), !signals, \\$flows, ^gates" >&2
|
|
462
|
+
echo " The stop hook WILL BLOCK if .purpose files aren't updated." >&2
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
exit 0
|
|
466
|
+
`;
|
|
467
|
+
var CLAUDE_CODE_PRECOMMIT_HOOK = `#!/bin/sh
|
|
468
|
+
# Paradigm Claude Code Pre-Commit Hook
|
|
469
|
+
# Intercepts git commit Bash calls and auto-rebuilds the index.
|
|
470
|
+
# Installed by: paradigm hooks install --claude-code
|
|
471
|
+
#
|
|
472
|
+
# Hook type: PreToolUse (matcher: Bash)
|
|
473
|
+
# Exit 0 = allow (never blocks), just ensures index is fresh
|
|
474
|
+
|
|
475
|
+
# Read JSON from stdin (hook input)
|
|
476
|
+
INPUT=$(cat)
|
|
477
|
+
|
|
478
|
+
# Extract the command from tool_input
|
|
479
|
+
if command -v jq >/dev/null 2>&1; then
|
|
480
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
481
|
+
else
|
|
482
|
+
COMMAND=$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"command"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
# If command doesn't contain "git commit", pass through
|
|
486
|
+
case "$COMMAND" in
|
|
487
|
+
*"git commit"*) ;;
|
|
488
|
+
*) exit 0 ;;
|
|
489
|
+
esac
|
|
490
|
+
|
|
491
|
+
# If no .paradigm directory, not a paradigm project
|
|
492
|
+
if [ ! -d ".paradigm" ]; then
|
|
493
|
+
exit 0
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
# Run paradigm index --quiet (the existing CLI command)
|
|
497
|
+
if command -v paradigm >/dev/null 2>&1; then
|
|
498
|
+
paradigm index --quiet 2>/dev/null || true
|
|
499
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
500
|
+
npx paradigm index --quiet 2>/dev/null || true
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# Stage the rebuilt files if they exist
|
|
504
|
+
for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index.json; do
|
|
505
|
+
if [ -f "$f" ]; then
|
|
506
|
+
git add "$f" 2>/dev/null || true
|
|
507
|
+
fi
|
|
508
|
+
done
|
|
509
|
+
|
|
510
|
+
# Never block \u2014 exit 0
|
|
511
|
+
exit 0
|
|
512
|
+
`;
|
|
513
|
+
var CURSOR_STOP_HOOK = `#!/bin/sh
|
|
514
|
+
# Paradigm Cursor Stop Hook (v2)
|
|
515
|
+
# Validates paradigm compliance before allowing the agent to finish.
|
|
516
|
+
# Installed by: paradigm hooks install --cursor
|
|
517
|
+
#
|
|
518
|
+
# Hook type: stop
|
|
519
|
+
# Exit 0 = allow, Exit 2 = block with message
|
|
520
|
+
#
|
|
521
|
+
# Checks:
|
|
522
|
+
# 1. Source files modified without .purpose updates (threshold: 2+)
|
|
523
|
+
# 2. Modified source directories missing .purpose files entirely
|
|
524
|
+
# 3. Route-like patterns added without portal.yaml updates
|
|
525
|
+
# 4. Aspect anchor files that no longer exist
|
|
526
|
+
# 5. Per-directory .purpose freshness (tracked via .pending-review)
|
|
527
|
+
# 6. Aspect coverage advisory
|
|
528
|
+
|
|
529
|
+
# Read JSON from stdin (hook input)
|
|
530
|
+
INPUT=$(cat)
|
|
531
|
+
|
|
532
|
+
# Extract workspace root from Cursor's input (try jq first, fallback to grep)
|
|
533
|
+
if command -v jq >/dev/null 2>&1; then
|
|
534
|
+
CWD=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
|
|
535
|
+
else
|
|
536
|
+
CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\\\["[^"]*"' | head -1 | sed 's/.*\\\\["//' | sed 's/"$//')
|
|
537
|
+
fi
|
|
538
|
+
|
|
539
|
+
if [ -z "$CWD" ]; then
|
|
540
|
+
CWD="$(pwd)"
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# Not a paradigm project \u2014 pass
|
|
544
|
+
if [ ! -d "$CWD/.paradigm" ]; then
|
|
545
|
+
exit 0
|
|
546
|
+
fi
|
|
547
|
+
|
|
548
|
+
cd "$CWD" || exit 0
|
|
549
|
+
|
|
550
|
+
# Get modified files (uncommitted changes)
|
|
551
|
+
MODIFIED=$(git diff --name-only HEAD 2>/dev/null)
|
|
552
|
+
if [ -z "$MODIFIED" ]; then
|
|
553
|
+
# Clean up pending-review on pass
|
|
554
|
+
rm -f ".paradigm/.pending-review"
|
|
555
|
+
exit 0
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
VIOLATIONS=""
|
|
559
|
+
VIOLATION_COUNT=0
|
|
560
|
+
|
|
561
|
+
# --- Check 1: Source files modified without .purpose updates ---
|
|
562
|
+
SOURCE_COUNT=0
|
|
563
|
+
PARADIGM_COUNT=0
|
|
564
|
+
|
|
565
|
+
for file in $MODIFIED; do
|
|
566
|
+
case "$file" in
|
|
567
|
+
.paradigm/*|*.purpose|portal.yaml)
|
|
568
|
+
PARADIGM_COUNT=$((PARADIGM_COUNT + 1))
|
|
569
|
+
;;
|
|
570
|
+
*.md|*.lock|*.log|.gitignore|.env*|*.json) ;;
|
|
571
|
+
*)
|
|
572
|
+
SOURCE_COUNT=$((SOURCE_COUNT + 1))
|
|
573
|
+
;;
|
|
574
|
+
esac
|
|
575
|
+
done
|
|
576
|
+
|
|
577
|
+
if [ "$SOURCE_COUNT" -gt 1 ] && [ "$PARADIGM_COUNT" -eq 0 ]; then
|
|
578
|
+
VIOLATIONS="$VIOLATIONS
|
|
579
|
+
- You modified $SOURCE_COUNT source files but 0 paradigm files (.purpose/portal.yaml).
|
|
580
|
+
Update the nearest .purpose file for each modified code area."
|
|
581
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
# --- Check 2: Modified source directories missing .purpose files ---
|
|
585
|
+
DIRS_WITHOUT_PURPOSE=""
|
|
586
|
+
|
|
587
|
+
for file in $MODIFIED; do
|
|
588
|
+
case "$file" in
|
|
589
|
+
.paradigm/*|*.md|*.lock|*.log|.gitignore|.env*|*.json|*.purpose|portal.yaml) continue ;;
|
|
590
|
+
esac
|
|
591
|
+
|
|
592
|
+
dir=$(dirname "$file")
|
|
593
|
+
# Walk up to find a .purpose file
|
|
594
|
+
found_purpose=false
|
|
595
|
+
check_dir="$dir"
|
|
596
|
+
while [ "$check_dir" != "." ] && [ "$check_dir" != "" ]; do
|
|
597
|
+
if [ -f "$check_dir/.purpose" ]; then
|
|
598
|
+
found_purpose=true
|
|
599
|
+
break
|
|
600
|
+
fi
|
|
601
|
+
check_dir=$(dirname "$check_dir")
|
|
602
|
+
done
|
|
603
|
+
# Also check root
|
|
604
|
+
if [ "$found_purpose" = false ] && [ -f ".purpose" ]; then
|
|
605
|
+
found_purpose=true
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
if [ "$found_purpose" = false ]; then
|
|
609
|
+
# Deduplicate directory names
|
|
610
|
+
case "$DIRS_WITHOUT_PURPOSE" in
|
|
611
|
+
*"$dir"*) ;;
|
|
612
|
+
*) DIRS_WITHOUT_PURPOSE="$DIRS_WITHOUT_PURPOSE $dir" ;;
|
|
613
|
+
esac
|
|
614
|
+
fi
|
|
615
|
+
done
|
|
616
|
+
|
|
617
|
+
if [ -n "$DIRS_WITHOUT_PURPOSE" ]; then
|
|
618
|
+
VIOLATIONS="$VIOLATIONS
|
|
619
|
+
- These directories have modified source files but no .purpose file anywhere in their path:
|
|
620
|
+
$DIRS_WITHOUT_PURPOSE
|
|
621
|
+
Create a .purpose file using paradigm_purpose_init + paradigm_purpose_add_component."
|
|
622
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
# --- Check 3: Route patterns added without portal.yaml ---
|
|
626
|
+
if [ -f "portal.yaml" ] || echo "$MODIFIED" | grep -q "portal.yaml"; then
|
|
627
|
+
: # portal.yaml exists or was modified \u2014 OK
|
|
628
|
+
else
|
|
629
|
+
# Check if any modified files contain route-like patterns
|
|
630
|
+
ROUTE_FILES=""
|
|
631
|
+
for file in $MODIFIED; do
|
|
632
|
+
case "$file" in
|
|
633
|
+
*.ts|*.js|*.tsx|*.jsx|*.py|*.rs|*.go)
|
|
634
|
+
if [ -f "$file" ]; then
|
|
635
|
+
if grep -qE '\\\\.(get|post|put|patch|delete)\\\\s*\\\\(|router\\\\.|app\\\\.(get|post|put|delete)|@(Get|Post|Put|Delete)|#\\\\[actix_web::(get|post)' "$file" 2>/dev/null; then
|
|
636
|
+
ROUTE_FILES="$ROUTE_FILES $file"
|
|
637
|
+
fi
|
|
638
|
+
fi
|
|
639
|
+
;;
|
|
640
|
+
esac
|
|
641
|
+
done
|
|
642
|
+
|
|
643
|
+
if [ -n "$ROUTE_FILES" ]; then
|
|
644
|
+
VIOLATIONS="$VIOLATIONS
|
|
645
|
+
- Route/endpoint patterns found in modified files but no portal.yaml exists:
|
|
646
|
+
$ROUTE_FILES
|
|
647
|
+
Create portal.yaml with gate definitions. Use paradigm_gates_for_route for suggestions."
|
|
648
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
649
|
+
fi
|
|
650
|
+
fi
|
|
651
|
+
|
|
652
|
+
# --- Check 4: Aspect anchor files that no longer exist ---
|
|
653
|
+
for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null); do
|
|
654
|
+
if grep -q "anchors:" "$purpose_file" 2>/dev/null; then
|
|
655
|
+
purpose_dir=$(dirname "$purpose_file")
|
|
656
|
+
in_anchors=false
|
|
657
|
+
while IFS= read -r line; do
|
|
658
|
+
case "$line" in
|
|
659
|
+
*"anchors:"*) in_anchors=true; continue ;;
|
|
660
|
+
*"- "*)
|
|
661
|
+
if [ "$in_anchors" = true ]; then
|
|
662
|
+
anchor_path=$(echo "$line" | sed 's/.*- //' | sed 's/:.*//' | tr -d ' ')
|
|
663
|
+
if [ -n "$anchor_path" ]; then
|
|
664
|
+
resolved_path="$purpose_dir/$anchor_path"
|
|
665
|
+
if [ ! -f "$resolved_path" ]; then
|
|
666
|
+
VIOLATIONS="$VIOLATIONS
|
|
667
|
+
- Aspect anchor '$anchor_path' in $purpose_file does not exist.
|
|
668
|
+
Update the anchor or remove the stale aspect."
|
|
669
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
670
|
+
fi
|
|
671
|
+
fi
|
|
672
|
+
fi
|
|
673
|
+
;;
|
|
674
|
+
*) in_anchors=false ;;
|
|
675
|
+
esac
|
|
676
|
+
done < "$purpose_file"
|
|
677
|
+
fi
|
|
678
|
+
done
|
|
679
|
+
|
|
680
|
+
# --- Check 5: Per-directory .purpose freshness ---
|
|
681
|
+
PENDING_FILE=".paradigm/.pending-review"
|
|
682
|
+
if [ -f "$PENDING_FILE" ]; then
|
|
683
|
+
STALE_PURPOSES=""
|
|
684
|
+
while IFS= read -r tracked_file; do
|
|
685
|
+
[ -z "$tracked_file" ] && continue
|
|
686
|
+
# Find covering .purpose for this tracked file
|
|
687
|
+
check_dir=$(dirname "$tracked_file")
|
|
688
|
+
covering_purpose=""
|
|
689
|
+
while [ "$check_dir" != "." ] && [ "$check_dir" != "" ]; do
|
|
690
|
+
if [ -f "$check_dir/.purpose" ]; then
|
|
691
|
+
covering_purpose="$check_dir/.purpose"
|
|
692
|
+
break
|
|
693
|
+
fi
|
|
694
|
+
check_dir=$(dirname "$check_dir")
|
|
695
|
+
done
|
|
696
|
+
if [ -z "$covering_purpose" ] && [ -f ".purpose" ]; then
|
|
697
|
+
covering_purpose=".purpose"
|
|
698
|
+
fi
|
|
699
|
+
# Check if covering .purpose was also modified
|
|
700
|
+
if [ -n "$covering_purpose" ]; then
|
|
701
|
+
if ! echo "$MODIFIED" | grep -qxF "$covering_purpose"; then
|
|
702
|
+
# Deduplicate
|
|
703
|
+
case "$STALE_PURPOSES" in
|
|
704
|
+
*"$covering_purpose"*) ;;
|
|
705
|
+
*) STALE_PURPOSES="$STALE_PURPOSES $covering_purpose" ;;
|
|
706
|
+
esac
|
|
707
|
+
fi
|
|
708
|
+
fi
|
|
709
|
+
done < "$PENDING_FILE"
|
|
710
|
+
|
|
711
|
+
if [ -n "$STALE_PURPOSES" ]; then
|
|
712
|
+
VIOLATIONS="$VIOLATIONS
|
|
713
|
+
- These .purpose files cover modified source code but were NOT updated:
|
|
714
|
+
$STALE_PURPOSES
|
|
715
|
+
Update each with: #components, ~aspects (with anchors), !signals, \\$flows, ^gates."
|
|
716
|
+
VIOLATION_COUNT=$((VIOLATION_COUNT + 1))
|
|
717
|
+
fi
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
# --- Check 6: Aspect coverage advisory ---
|
|
721
|
+
ADVISORY=""
|
|
722
|
+
HAS_ASPECTS=false
|
|
723
|
+
for purpose_file in $(find . -name ".purpose" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null); do
|
|
724
|
+
if grep -qE '^\\s*~' "$purpose_file" 2>/dev/null; then
|
|
725
|
+
HAS_ASPECTS=true
|
|
726
|
+
break
|
|
727
|
+
fi
|
|
728
|
+
done
|
|
729
|
+
|
|
730
|
+
if [ "$HAS_ASPECTS" = true ] && [ "$SOURCE_COUNT" -gt 0 ]; then
|
|
731
|
+
ASPECT_UPDATED=false
|
|
732
|
+
for file in $MODIFIED; do
|
|
733
|
+
case "$file" in
|
|
734
|
+
*.purpose)
|
|
735
|
+
if grep -qE '^\\s*~|anchors:|applies-to:' "$file" 2>/dev/null; then
|
|
736
|
+
ASPECT_UPDATED=true
|
|
737
|
+
break
|
|
738
|
+
fi
|
|
739
|
+
;;
|
|
740
|
+
esac
|
|
741
|
+
done
|
|
742
|
+
|
|
743
|
+
if [ "$ASPECT_UPDATED" = false ]; then
|
|
744
|
+
ADVISORY=" This project defines ~aspects with code anchors. Check if existing
|
|
745
|
+
~aspects need updated anchors or applies-to patterns."
|
|
746
|
+
fi
|
|
747
|
+
fi
|
|
748
|
+
|
|
749
|
+
# --- Final verdict ---
|
|
750
|
+
if [ "$VIOLATION_COUNT" -gt 0 ]; then
|
|
751
|
+
echo "" >&2
|
|
752
|
+
echo "Paradigm compliance check failed ($VIOLATION_COUNT violation(s)):" >&2
|
|
753
|
+
echo "$VIOLATIONS" >&2
|
|
754
|
+
if [ -n "$ADVISORY" ]; then
|
|
755
|
+
echo "" >&2
|
|
756
|
+
echo "Advisory:" >&2
|
|
757
|
+
echo "$ADVISORY" >&2
|
|
758
|
+
fi
|
|
759
|
+
echo "" >&2
|
|
760
|
+
echo "Run these MCP tools to fix:" >&2
|
|
761
|
+
echo " 1. paradigm_purpose_add_component \u2014 register new code units" >&2
|
|
762
|
+
echo " 2. paradigm_purpose_add_aspect \u2014 register cross-cutting concerns (with anchors)" >&2
|
|
763
|
+
echo " 3. paradigm_portal_add_route \u2014 register new endpoints with gates" >&2
|
|
764
|
+
echo " 4. paradigm_reindex \u2014 rebuild indexes after updates" >&2
|
|
765
|
+
exit 2
|
|
766
|
+
fi
|
|
767
|
+
|
|
768
|
+
# Print advisory even on pass (informational)
|
|
769
|
+
if [ -n "$ADVISORY" ]; then
|
|
770
|
+
echo "" >&2
|
|
771
|
+
echo "[paradigm] Advisory:" >&2
|
|
772
|
+
echo "$ADVISORY" >&2
|
|
773
|
+
fi
|
|
774
|
+
|
|
775
|
+
# Clean up pending-review on pass
|
|
776
|
+
rm -f ".paradigm/.pending-review"
|
|
777
|
+
|
|
778
|
+
exit 0
|
|
779
|
+
`;
|
|
780
|
+
var CURSOR_POSTWRITE_HOOK = `#!/bin/sh
|
|
781
|
+
# Paradigm Cursor PostWrite Hook (v2)
|
|
782
|
+
# Fires after file edits.
|
|
783
|
+
# Tracks modified source files in .paradigm/.pending-review
|
|
784
|
+
# and outputs compliance reminders.
|
|
785
|
+
# Installed by: paradigm hooks install --cursor
|
|
786
|
+
#
|
|
787
|
+
# Hook type: afterFileEdit
|
|
788
|
+
# Exit 0 always (never blocks \u2014 advisory only)
|
|
789
|
+
|
|
790
|
+
# Read JSON from stdin (hook input)
|
|
791
|
+
INPUT=$(cat)
|
|
792
|
+
|
|
793
|
+
# Extract file path from Cursor's afterFileEdit input
|
|
794
|
+
if command -v jq >/dev/null 2>&1; then
|
|
795
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.file // .filePath // empty' 2>/dev/null)
|
|
796
|
+
else
|
|
797
|
+
FILE_PATH=$(echo "$INPUT" | grep -o '"file"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
798
|
+
if [ -z "$FILE_PATH" ]; then
|
|
799
|
+
FILE_PATH=$(echo "$INPUT" | grep -o '"filePath"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"filePath"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
800
|
+
fi
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
if [ -z "$FILE_PATH" ]; then
|
|
804
|
+
exit 0
|
|
805
|
+
fi
|
|
806
|
+
|
|
807
|
+
# Skip non-source files
|
|
808
|
+
case "$FILE_PATH" in
|
|
809
|
+
*.purpose|portal.yaml|*.md|*.lock|*.log|*.json|*.yaml|*.yml|.gitignore|.env*) exit 0 ;;
|
|
810
|
+
esac
|
|
811
|
+
|
|
812
|
+
# Skip .paradigm, .claude, and .cursor directories
|
|
813
|
+
case "$FILE_PATH" in
|
|
814
|
+
*/.paradigm/*|.paradigm/*|*/.claude/*|.claude/*|*/.cursor/*|.cursor/*) exit 0 ;;
|
|
815
|
+
esac
|
|
816
|
+
|
|
817
|
+
# Not a paradigm project \u2014 pass
|
|
818
|
+
if [ ! -d ".paradigm" ]; then
|
|
819
|
+
exit 0
|
|
820
|
+
fi
|
|
821
|
+
|
|
822
|
+
# Convert to relative path (strip project root prefix)
|
|
823
|
+
PROJECT_ROOT="$(pwd)"
|
|
824
|
+
REL_PATH="$FILE_PATH"
|
|
825
|
+
case "$FILE_PATH" in
|
|
826
|
+
"$PROJECT_ROOT"/*) REL_PATH=$(echo "$FILE_PATH" | sed "s|^$PROJECT_ROOT/||") ;;
|
|
827
|
+
esac
|
|
828
|
+
|
|
829
|
+
# If still absolute, file is outside project \u2014 skip
|
|
830
|
+
case "$REL_PATH" in
|
|
831
|
+
/*) exit 0 ;;
|
|
832
|
+
esac
|
|
833
|
+
|
|
834
|
+
# Track: append to .paradigm/.pending-review (deduplicated)
|
|
835
|
+
PENDING_FILE=".paradigm/.pending-review"
|
|
836
|
+
if [ -f "$PENDING_FILE" ]; then
|
|
837
|
+
if ! grep -qxF "$REL_PATH" "$PENDING_FILE" 2>/dev/null; then
|
|
838
|
+
echo "$REL_PATH" >> "$PENDING_FILE"
|
|
839
|
+
fi
|
|
840
|
+
else
|
|
841
|
+
echo "$REL_PATH" > "$PENDING_FILE"
|
|
842
|
+
fi
|
|
843
|
+
|
|
844
|
+
# Count pending files
|
|
845
|
+
PENDING_COUNT=$(wc -l < "$PENDING_FILE" | tr -d ' ')
|
|
846
|
+
|
|
847
|
+
# Walk up from the file's directory to find a .purpose file
|
|
848
|
+
dir=$(dirname "$REL_PATH")
|
|
849
|
+
found_purpose=""
|
|
850
|
+
|
|
851
|
+
while [ "$dir" != "." ] && [ "$dir" != "/" ] && [ "$dir" != "" ]; do
|
|
852
|
+
if [ -f "$dir/.purpose" ]; then
|
|
853
|
+
found_purpose="$dir/.purpose"
|
|
854
|
+
break
|
|
855
|
+
fi
|
|
856
|
+
dir=$(dirname "$dir")
|
|
857
|
+
done
|
|
858
|
+
|
|
859
|
+
# Check root .purpose
|
|
860
|
+
if [ -z "$found_purpose" ] && [ -f ".purpose" ]; then
|
|
861
|
+
found_purpose=".purpose"
|
|
862
|
+
fi
|
|
863
|
+
|
|
864
|
+
if [ -z "$found_purpose" ]; then
|
|
865
|
+
file_dir=$(dirname "$REL_PATH")
|
|
866
|
+
echo "" >&2
|
|
867
|
+
echo "[paradigm] No .purpose file covers $file_dir/" >&2
|
|
868
|
+
echo " Create one: paradigm_purpose_init + paradigm_purpose_add_component" >&2
|
|
869
|
+
echo " $PENDING_COUNT file(s) pending review. The stop hook WILL BLOCK." >&2
|
|
870
|
+
elif [ "$PENDING_COUNT" -gt 0 ] && [ "$((PENDING_COUNT % 3))" -eq 0 ]; then
|
|
871
|
+
echo "" >&2
|
|
872
|
+
echo "[paradigm] $PENDING_COUNT source file(s) modified. Update $found_purpose:" >&2
|
|
873
|
+
echo " -> #components, ~aspects (with anchors), !signals, \\$flows, ^gates" >&2
|
|
874
|
+
echo " The stop hook WILL BLOCK if .purpose files aren't updated." >&2
|
|
875
|
+
fi
|
|
876
|
+
|
|
877
|
+
exit 0
|
|
878
|
+
`;
|
|
879
|
+
var CURSOR_PRECOMMIT_HOOK = `#!/bin/sh
|
|
880
|
+
# Paradigm Cursor Pre-Commit Hook
|
|
881
|
+
# Intercepts git commit shell executions and auto-rebuilds the index.
|
|
882
|
+
# Installed by: paradigm hooks install --cursor
|
|
883
|
+
#
|
|
884
|
+
# Hook type: beforeShellExecution (matcher: "git commit")
|
|
885
|
+
# Exit 0 = allow (never blocks), just ensures index is fresh
|
|
886
|
+
|
|
887
|
+
# Read JSON from stdin (hook input)
|
|
888
|
+
INPUT=$(cat)
|
|
889
|
+
|
|
890
|
+
# Extract the command from Cursor's beforeShellExecution input
|
|
891
|
+
if command -v jq >/dev/null 2>&1; then
|
|
892
|
+
COMMAND=$(echo "$INPUT" | jq -r '.command // .shellCommand // empty' 2>/dev/null)
|
|
893
|
+
else
|
|
894
|
+
COMMAND=$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"command"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
|
|
895
|
+
fi
|
|
896
|
+
|
|
897
|
+
# If command doesn't contain "git commit", pass through
|
|
898
|
+
case "$COMMAND" in
|
|
899
|
+
*"git commit"*) ;;
|
|
900
|
+
*) exit 0 ;;
|
|
901
|
+
esac
|
|
902
|
+
|
|
903
|
+
# If no .paradigm directory, not a paradigm project
|
|
904
|
+
if [ ! -d ".paradigm" ]; then
|
|
905
|
+
exit 0
|
|
906
|
+
fi
|
|
907
|
+
|
|
908
|
+
# Run paradigm index --quiet (the existing CLI command)
|
|
909
|
+
if command -v paradigm >/dev/null 2>&1; then
|
|
910
|
+
paradigm index --quiet 2>/dev/null || true
|
|
911
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
912
|
+
npx paradigm index --quiet 2>/dev/null || true
|
|
913
|
+
fi
|
|
914
|
+
|
|
915
|
+
# Stage the rebuilt files if they exist
|
|
916
|
+
for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index.json; do
|
|
917
|
+
if [ -f "$f" ]; then
|
|
918
|
+
git add "$f" 2>/dev/null || true
|
|
919
|
+
fi
|
|
920
|
+
done
|
|
921
|
+
|
|
922
|
+
# Never block \u2014 exit 0
|
|
923
|
+
exit 0
|
|
924
|
+
`;
|
|
925
|
+
async function hooksInstallCommand(options = {}) {
|
|
926
|
+
const rootDir = process.cwd();
|
|
927
|
+
const onlyClaudeCode = options.claudeCode && !options.postCommit && !options.prePush && !options.cursor;
|
|
928
|
+
const onlyCursor = options.cursor && !options.postCommit && !options.prePush && !options.claudeCode;
|
|
929
|
+
if (!onlyClaudeCode && !onlyCursor) {
|
|
930
|
+
const gitDir = path.join(rootDir, ".git");
|
|
931
|
+
if (!fs.existsSync(gitDir)) {
|
|
932
|
+
console.log(chalk.red("Not a git repository."));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
936
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
937
|
+
const installAll2 = !options.postCommit && !options.prePush && !options.claudeCode;
|
|
938
|
+
const installed = [];
|
|
939
|
+
if (installAll2 || options.postCommit) {
|
|
940
|
+
const hookPath = path.join(hooksDir, "post-commit");
|
|
941
|
+
if (fs.existsSync(hookPath) && !options.force) {
|
|
942
|
+
const content = fs.readFileSync(hookPath, "utf8");
|
|
943
|
+
if (!content.includes("paradigm")) {
|
|
944
|
+
console.log(chalk.yellow("post-commit hook exists. Use --force to overwrite."));
|
|
945
|
+
} else {
|
|
946
|
+
console.log(chalk.gray("post-commit hook already installed by paradigm"));
|
|
947
|
+
}
|
|
948
|
+
} else {
|
|
949
|
+
fs.writeFileSync(hookPath, POST_COMMIT_HOOK);
|
|
950
|
+
fs.chmodSync(hookPath, "755");
|
|
951
|
+
installed.push("post-commit");
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (installAll2 || options.prePush) {
|
|
955
|
+
const hookPath = path.join(hooksDir, "pre-push");
|
|
956
|
+
if (fs.existsSync(hookPath) && !options.force) {
|
|
957
|
+
const content = fs.readFileSync(hookPath, "utf8");
|
|
958
|
+
if (!content.includes("paradigm")) {
|
|
959
|
+
console.log(chalk.yellow("pre-push hook exists. Use --force to overwrite."));
|
|
960
|
+
} else {
|
|
961
|
+
console.log(chalk.gray("pre-push hook already installed by paradigm"));
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
fs.writeFileSync(hookPath, PRE_PUSH_HOOK);
|
|
965
|
+
fs.chmodSync(hookPath, "755");
|
|
966
|
+
installed.push("pre-push");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (installed.length > 0) {
|
|
970
|
+
console.log(chalk.green(`Git hooks installed: ${installed.join(", ")}`));
|
|
971
|
+
}
|
|
972
|
+
const historyDir = path.join(rootDir, ".paradigm/history");
|
|
973
|
+
if (!fs.existsSync(historyDir)) {
|
|
974
|
+
console.log(chalk.gray("Tip: Run `paradigm history init` to initialize history tracking"));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const installAll = !options.postCommit && !options.prePush && !options.claudeCode && !options.cursor;
|
|
978
|
+
if (installAll || options.claudeCode) {
|
|
979
|
+
await installClaudeCodeHooks(rootDir, options.force);
|
|
980
|
+
}
|
|
981
|
+
if (installAll || options.cursor) {
|
|
982
|
+
await installCursorHooks(rootDir, options.force);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
async function installClaudeCodeHooks(rootDir, force) {
|
|
986
|
+
const claudeHooksDir = path.join(rootDir, ".claude", "hooks");
|
|
987
|
+
fs.mkdirSync(claudeHooksDir, { recursive: true });
|
|
988
|
+
const installed = [];
|
|
989
|
+
const hookScripts = [
|
|
990
|
+
{ name: "paradigm-stop.sh", content: CLAUDE_CODE_STOP_HOOK },
|
|
991
|
+
{ name: "paradigm-precommit.sh", content: CLAUDE_CODE_PRECOMMIT_HOOK },
|
|
992
|
+
{ name: "paradigm-postwrite.sh", content: CLAUDE_CODE_POSTWRITE_HOOK }
|
|
993
|
+
];
|
|
994
|
+
for (const hook of hookScripts) {
|
|
995
|
+
const destPath = path.join(claudeHooksDir, hook.name);
|
|
996
|
+
if (fs.existsSync(destPath) && !force) {
|
|
997
|
+
console.log(chalk.gray(` ${hook.name}: already installed`));
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
fs.writeFileSync(destPath, hook.content, "utf8");
|
|
1001
|
+
fs.chmodSync(destPath, "755");
|
|
1002
|
+
installed.push(hook.name);
|
|
1003
|
+
}
|
|
1004
|
+
const settingsPath = path.join(rootDir, ".claude", "settings.json");
|
|
1005
|
+
let settings = {};
|
|
1006
|
+
if (fs.existsSync(settingsPath)) {
|
|
1007
|
+
try {
|
|
1008
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const hooks = settings.hooks || {};
|
|
1013
|
+
const stopHookEntry = {
|
|
1014
|
+
hooks: [{
|
|
1015
|
+
type: "command",
|
|
1016
|
+
command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/paradigm-stop.sh"`,
|
|
1017
|
+
timeout: 10
|
|
1018
|
+
}]
|
|
1019
|
+
};
|
|
1020
|
+
const preCommitHookEntry = {
|
|
1021
|
+
matcher: "Bash",
|
|
1022
|
+
hooks: [{
|
|
1023
|
+
type: "command",
|
|
1024
|
+
command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/paradigm-precommit.sh"`,
|
|
1025
|
+
timeout: 30
|
|
1026
|
+
}]
|
|
1027
|
+
};
|
|
1028
|
+
const stopHooks = hooks.Stop || [];
|
|
1029
|
+
const hasParadigmStop = stopHooks.some(
|
|
1030
|
+
(h) => JSON.stringify(h).includes("paradigm-stop.sh")
|
|
1031
|
+
);
|
|
1032
|
+
if (!hasParadigmStop) {
|
|
1033
|
+
stopHooks.push(stopHookEntry);
|
|
1034
|
+
}
|
|
1035
|
+
hooks.Stop = stopHooks;
|
|
1036
|
+
const preToolUseHooks = hooks.PreToolUse || [];
|
|
1037
|
+
const hasParadigmPrecommit = preToolUseHooks.some(
|
|
1038
|
+
(h) => JSON.stringify(h).includes("paradigm-precommit.sh")
|
|
1039
|
+
);
|
|
1040
|
+
if (!hasParadigmPrecommit) {
|
|
1041
|
+
preToolUseHooks.push(preCommitHookEntry);
|
|
1042
|
+
}
|
|
1043
|
+
hooks.PreToolUse = preToolUseHooks;
|
|
1044
|
+
const postWriteHookEntry = {
|
|
1045
|
+
matcher: "Edit,Write",
|
|
1046
|
+
hooks: [{
|
|
1047
|
+
type: "command",
|
|
1048
|
+
command: `bash "$CLAUDE_PROJECT_DIR/.claude/hooks/paradigm-postwrite.sh"`,
|
|
1049
|
+
timeout: 5
|
|
1050
|
+
}]
|
|
1051
|
+
};
|
|
1052
|
+
const postToolUseHooks = hooks.PostToolUse || [];
|
|
1053
|
+
const hasParadigmPostwrite = postToolUseHooks.some(
|
|
1054
|
+
(h) => JSON.stringify(h).includes("paradigm-postwrite.sh")
|
|
1055
|
+
);
|
|
1056
|
+
if (!hasParadigmPostwrite) {
|
|
1057
|
+
postToolUseHooks.push(postWriteHookEntry);
|
|
1058
|
+
}
|
|
1059
|
+
hooks.PostToolUse = postToolUseHooks;
|
|
1060
|
+
settings.hooks = hooks;
|
|
1061
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1062
|
+
if (installed.length > 0) {
|
|
1063
|
+
console.log(chalk.green(`Claude Code hooks installed: ${installed.join(", ")}`));
|
|
1064
|
+
}
|
|
1065
|
+
console.log(chalk.green("Claude Code settings.json updated with hook configuration"));
|
|
1066
|
+
}
|
|
1067
|
+
async function installCursorHooks(rootDir, force) {
|
|
1068
|
+
const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
|
|
1069
|
+
fs.mkdirSync(cursorHooksDir, { recursive: true });
|
|
1070
|
+
const installed = [];
|
|
1071
|
+
const hookScripts = [
|
|
1072
|
+
{ name: "paradigm-stop.sh", content: CURSOR_STOP_HOOK },
|
|
1073
|
+
{ name: "paradigm-precommit.sh", content: CURSOR_PRECOMMIT_HOOK },
|
|
1074
|
+
{ name: "paradigm-postwrite.sh", content: CURSOR_POSTWRITE_HOOK }
|
|
1075
|
+
];
|
|
1076
|
+
for (const hook of hookScripts) {
|
|
1077
|
+
const destPath = path.join(cursorHooksDir, hook.name);
|
|
1078
|
+
if (fs.existsSync(destPath) && !force) {
|
|
1079
|
+
console.log(chalk.gray(` ${hook.name}: already installed (Cursor)`));
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
fs.writeFileSync(destPath, hook.content, "utf8");
|
|
1083
|
+
fs.chmodSync(destPath, "755");
|
|
1084
|
+
installed.push(hook.name);
|
|
1085
|
+
}
|
|
1086
|
+
const hooksJsonPath = path.join(rootDir, ".cursor", "hooks.json");
|
|
1087
|
+
let hooksConfig = {};
|
|
1088
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
1089
|
+
try {
|
|
1090
|
+
hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
|
|
1091
|
+
} catch {
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
hooksConfig.version = 1;
|
|
1095
|
+
const hooks = hooksConfig.hooks || {};
|
|
1096
|
+
const paradigmStopEntry = {
|
|
1097
|
+
command: ".cursor/hooks/paradigm-stop.sh",
|
|
1098
|
+
timeout: 10
|
|
1099
|
+
};
|
|
1100
|
+
const paradigmPostwriteEntry = {
|
|
1101
|
+
command: ".cursor/hooks/paradigm-postwrite.sh",
|
|
1102
|
+
timeout: 5
|
|
1103
|
+
};
|
|
1104
|
+
const paradigmPrecommitEntry = {
|
|
1105
|
+
command: ".cursor/hooks/paradigm-precommit.sh",
|
|
1106
|
+
matcher: "git commit",
|
|
1107
|
+
timeout: 30
|
|
1108
|
+
};
|
|
1109
|
+
const stopHooks = hooks.stop || [];
|
|
1110
|
+
const hasParadigmStop = stopHooks.some(
|
|
1111
|
+
(h) => JSON.stringify(h).includes("paradigm-stop.sh")
|
|
1112
|
+
);
|
|
1113
|
+
if (!hasParadigmStop) {
|
|
1114
|
+
stopHooks.push(paradigmStopEntry);
|
|
1115
|
+
}
|
|
1116
|
+
hooks.stop = stopHooks;
|
|
1117
|
+
const afterFileEditHooks = hooks.afterFileEdit || [];
|
|
1118
|
+
const hasParadigmPostwrite = afterFileEditHooks.some(
|
|
1119
|
+
(h) => JSON.stringify(h).includes("paradigm-postwrite.sh")
|
|
1120
|
+
);
|
|
1121
|
+
if (!hasParadigmPostwrite) {
|
|
1122
|
+
afterFileEditHooks.push(paradigmPostwriteEntry);
|
|
1123
|
+
}
|
|
1124
|
+
hooks.afterFileEdit = afterFileEditHooks;
|
|
1125
|
+
const beforeShellHooks = hooks.beforeShellExecution || [];
|
|
1126
|
+
const hasParadigmPrecommit = beforeShellHooks.some(
|
|
1127
|
+
(h) => JSON.stringify(h).includes("paradigm-precommit.sh")
|
|
1128
|
+
);
|
|
1129
|
+
if (!hasParadigmPrecommit) {
|
|
1130
|
+
beforeShellHooks.push(paradigmPrecommitEntry);
|
|
1131
|
+
}
|
|
1132
|
+
hooks.beforeShellExecution = beforeShellHooks;
|
|
1133
|
+
hooksConfig.hooks = hooks;
|
|
1134
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
|
|
1135
|
+
if (installed.length > 0) {
|
|
1136
|
+
console.log(chalk.green(`Cursor hooks installed: ${installed.join(", ")}`));
|
|
1137
|
+
}
|
|
1138
|
+
console.log(chalk.green("Cursor hooks.json updated with hook configuration"));
|
|
1139
|
+
}
|
|
1140
|
+
async function hooksUninstallCommand(options = {}) {
|
|
1141
|
+
const rootDir = process.cwd();
|
|
1142
|
+
if (!options.cursor) {
|
|
1143
|
+
const gitDir = path.join(rootDir, ".git");
|
|
1144
|
+
if (!fs.existsSync(gitDir)) {
|
|
1145
|
+
console.log(chalk.red("Not a git repository."));
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
1149
|
+
const removed = [];
|
|
1150
|
+
for (const hookName of ["post-commit", "pre-push"]) {
|
|
1151
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
1152
|
+
if (fs.existsSync(hookPath)) {
|
|
1153
|
+
const content = fs.readFileSync(hookPath, "utf8");
|
|
1154
|
+
if (content.includes("paradigm")) {
|
|
1155
|
+
fs.unlinkSync(hookPath);
|
|
1156
|
+
removed.push(hookName);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (removed.length > 0) {
|
|
1161
|
+
console.log(chalk.green(`Git hooks removed: ${removed.join(", ")}`));
|
|
1162
|
+
} else {
|
|
1163
|
+
console.log(chalk.gray("No paradigm git hooks found to remove"));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (options.cursor) {
|
|
1167
|
+
const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
|
|
1168
|
+
const cursorRemoved = [];
|
|
1169
|
+
for (const hookName of ["paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh"]) {
|
|
1170
|
+
const hookPath = path.join(cursorHooksDir, hookName);
|
|
1171
|
+
if (fs.existsSync(hookPath)) {
|
|
1172
|
+
fs.unlinkSync(hookPath);
|
|
1173
|
+
cursorRemoved.push(hookName);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const hooksJsonPath = path.join(rootDir, ".cursor", "hooks.json");
|
|
1177
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
1178
|
+
try {
|
|
1179
|
+
const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
|
|
1180
|
+
const hooks = hooksConfig.hooks || {};
|
|
1181
|
+
for (const key of ["stop", "afterFileEdit", "beforeShellExecution"]) {
|
|
1182
|
+
if (Array.isArray(hooks[key])) {
|
|
1183
|
+
hooks[key] = hooks[key].filter(
|
|
1184
|
+
(h) => !JSON.stringify(h).includes("paradigm-")
|
|
1185
|
+
);
|
|
1186
|
+
if (hooks[key].length === 0) {
|
|
1187
|
+
delete hooks[key];
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
hooksConfig.hooks = hooks;
|
|
1192
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (cursorRemoved.length > 0) {
|
|
1197
|
+
console.log(chalk.green(`Cursor hooks removed: ${cursorRemoved.join(", ")}`));
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(chalk.gray("No paradigm Cursor hooks found to remove"));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
async function hooksStatusCommand() {
|
|
1204
|
+
const rootDir = process.cwd();
|
|
1205
|
+
const gitDir = path.join(rootDir, ".git");
|
|
1206
|
+
if (fs.existsSync(gitDir)) {
|
|
1207
|
+
console.log(chalk.magenta("\n Git Hooks Status\n"));
|
|
1208
|
+
const hooksDir = path.join(gitDir, "hooks");
|
|
1209
|
+
const hooks = ["post-commit", "pre-push"];
|
|
1210
|
+
for (const hookName of hooks) {
|
|
1211
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
1212
|
+
if (fs.existsSync(hookPath)) {
|
|
1213
|
+
const content = fs.readFileSync(hookPath, "utf8");
|
|
1214
|
+
if (content.includes("paradigm")) {
|
|
1215
|
+
console.log(chalk.green(` ${hookName}: installed (paradigm)`));
|
|
1216
|
+
} else {
|
|
1217
|
+
console.log(chalk.yellow(` ${hookName}: exists (other)`));
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
console.log(chalk.gray(` ${hookName}: not installed`));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
console.log();
|
|
1224
|
+
const historyDir = path.join(rootDir, ".paradigm/history");
|
|
1225
|
+
if (fs.existsSync(historyDir)) {
|
|
1226
|
+
const logPath = path.join(historyDir, "log.jsonl");
|
|
1227
|
+
if (fs.existsSync(logPath)) {
|
|
1228
|
+
const content = fs.readFileSync(logPath, "utf8");
|
|
1229
|
+
const count = content.split("\n").filter((l) => l.trim()).length;
|
|
1230
|
+
console.log(chalk.white(` History entries: ${count}`));
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
console.log(chalk.gray(" History: not initialized"));
|
|
1234
|
+
console.log(chalk.gray(" Run `paradigm history init` to enable"));
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
console.log(chalk.gray("\n Not a git repository (git hooks N/A)\n"));
|
|
1238
|
+
}
|
|
1239
|
+
console.log(chalk.magenta(" Claude Code Hooks Status\n"));
|
|
1240
|
+
const claudeHooksDir = path.join(rootDir, ".claude", "hooks");
|
|
1241
|
+
const claudeHooks = ["paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh"];
|
|
1242
|
+
for (const hookName of claudeHooks) {
|
|
1243
|
+
const hookPath = path.join(claudeHooksDir, hookName);
|
|
1244
|
+
if (fs.existsSync(hookPath)) {
|
|
1245
|
+
console.log(chalk.green(` ${hookName}: installed`));
|
|
1246
|
+
} else {
|
|
1247
|
+
console.log(chalk.gray(` ${hookName}: not installed`));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const settingsPath = path.join(rootDir, ".claude", "settings.json");
|
|
1251
|
+
if (fs.existsSync(settingsPath)) {
|
|
1252
|
+
try {
|
|
1253
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
1254
|
+
const hooks = settings.hooks || {};
|
|
1255
|
+
const hasStop = JSON.stringify(hooks.Stop || []).includes("paradigm-stop.sh");
|
|
1256
|
+
const hasPrecommit = JSON.stringify(hooks.PreToolUse || []).includes("paradigm-precommit.sh");
|
|
1257
|
+
const hasPostwrite = JSON.stringify(hooks.PostToolUse || []).includes("paradigm-postwrite.sh");
|
|
1258
|
+
console.log(chalk.gray(` settings.json Stop hook: ${hasStop ? "configured" : "missing"}`));
|
|
1259
|
+
console.log(chalk.gray(` settings.json PreToolUse hook: ${hasPrecommit ? "configured" : "missing"}`));
|
|
1260
|
+
console.log(chalk.gray(` settings.json PostToolUse hook: ${hasPostwrite ? "configured" : "missing"}`));
|
|
1261
|
+
} catch {
|
|
1262
|
+
console.log(chalk.yellow(" settings.json: parse error"));
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
console.log(chalk.gray(" settings.json: not found"));
|
|
1266
|
+
}
|
|
1267
|
+
console.log(chalk.magenta("\n Cursor Hooks Status\n"));
|
|
1268
|
+
const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
|
|
1269
|
+
const cursorHooks = ["paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh"];
|
|
1270
|
+
for (const hookName of cursorHooks) {
|
|
1271
|
+
const hookPath = path.join(cursorHooksDir, hookName);
|
|
1272
|
+
if (fs.existsSync(hookPath)) {
|
|
1273
|
+
console.log(chalk.green(` ${hookName}: installed`));
|
|
1274
|
+
} else {
|
|
1275
|
+
console.log(chalk.gray(` ${hookName}: not installed`));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const cursorHooksJsonPath = path.join(rootDir, ".cursor", "hooks.json");
|
|
1279
|
+
if (fs.existsSync(cursorHooksJsonPath)) {
|
|
1280
|
+
try {
|
|
1281
|
+
const hooksJson = JSON.parse(fs.readFileSync(cursorHooksJsonPath, "utf8"));
|
|
1282
|
+
const hooks = hooksJson.hooks || {};
|
|
1283
|
+
const hasStop = JSON.stringify(hooks.stop || []).includes("paradigm-stop.sh");
|
|
1284
|
+
const hasPostwrite = JSON.stringify(hooks.afterFileEdit || []).includes("paradigm-postwrite.sh");
|
|
1285
|
+
const hasPrecommit = JSON.stringify(hooks.beforeShellExecution || []).includes("paradigm-precommit.sh");
|
|
1286
|
+
console.log(chalk.gray(` hooks.json stop: ${hasStop ? "configured" : "missing"}`));
|
|
1287
|
+
console.log(chalk.gray(` hooks.json afterFileEdit: ${hasPostwrite ? "configured" : "missing"}`));
|
|
1288
|
+
console.log(chalk.gray(` hooks.json beforeShellExecution: ${hasPrecommit ? "configured" : "missing"}`));
|
|
1289
|
+
} catch {
|
|
1290
|
+
console.log(chalk.yellow(" hooks.json: parse error"));
|
|
1291
|
+
}
|
|
1292
|
+
} else {
|
|
1293
|
+
console.log(chalk.gray(" hooks.json: not found"));
|
|
1294
|
+
}
|
|
1295
|
+
console.log();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
export {
|
|
1299
|
+
hooksInstallCommand,
|
|
1300
|
+
hooksUninstallCommand,
|
|
1301
|
+
hooksStatusCommand
|
|
1302
|
+
};
|