@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.
Files changed (114) hide show
  1. package/README.md +142 -0
  2. package/dist/accept-orchestration-CWZNCGZX.js +188 -0
  3. package/dist/agents-suggest-35LIQKDH.js +83 -0
  4. package/dist/aggregate-W7Q6VIM2.js +88 -0
  5. package/dist/auto-IU7VN55K.js +470 -0
  6. package/dist/beacon-B47XSTL7.js +251 -0
  7. package/dist/chunk-2M6OSOIG.js +1302 -0
  8. package/dist/chunk-4NCFWYGG.js +110 -0
  9. package/dist/chunk-5C4SGQKH.js +705 -0
  10. package/dist/chunk-5GOA7WYD.js +1095 -0
  11. package/dist/chunk-5JGJACDU.js +37 -0
  12. package/dist/chunk-6QC3YGB6.js +114 -0
  13. package/dist/chunk-753RICFF.js +325 -0
  14. package/dist/chunk-AD2LSCHB.js +1595 -0
  15. package/dist/chunk-CHSHON3O.js +669 -0
  16. package/dist/chunk-ELLR7WP6.js +3175 -0
  17. package/dist/chunk-ILOWBJRC.js +12 -0
  18. package/dist/chunk-IRKUEJVW.js +405 -0
  19. package/dist/chunk-MC7XC7XQ.js +533 -0
  20. package/dist/chunk-MO4EEYFW.js +38 -0
  21. package/dist/chunk-MQWH7PFI.js +13366 -0
  22. package/dist/chunk-N6PJAPDE.js +364 -0
  23. package/dist/chunk-PBHIFAL4.js +259 -0
  24. package/dist/chunk-PMXRGPRQ.js +305 -0
  25. package/dist/chunk-PW2EXJQT.js +689 -0
  26. package/dist/chunk-TAP5N3HH.js +245 -0
  27. package/dist/chunk-THFVK5AE.js +148 -0
  28. package/dist/chunk-UM54F7G5.js +1533 -0
  29. package/dist/chunk-UUZ2DMG5.js +185 -0
  30. package/dist/chunk-WS5KM7OL.js +780 -0
  31. package/dist/chunk-YDNKXH4Z.js +2316 -0
  32. package/dist/chunk-YO6DVTL7.js +99 -0
  33. package/dist/claude-SUYNN72C.js +362 -0
  34. package/dist/claude-cli-OF43XAO3.js +276 -0
  35. package/dist/claude-code-PW6SKD2M.js +126 -0
  36. package/dist/claude-code-teams-JLZ5IXB6.js +199 -0
  37. package/dist/constellation-K3CIQCHI.js +225 -0
  38. package/dist/cost-AEK6R7HK.js +174 -0
  39. package/dist/cost-KYXIQ62X.js +93 -0
  40. package/dist/cursor-cli-IHJMPRCW.js +269 -0
  41. package/dist/cursorrules-KI5QWHIX.js +84 -0
  42. package/dist/diff-AJJ5H6HV.js +125 -0
  43. package/dist/dist-7MPIRMTZ-IOQOREMZ.js +10866 -0
  44. package/dist/dist-NHJQVVUW.js +68 -0
  45. package/dist/dist-ZEMSQV74.js +20 -0
  46. package/dist/doctor-6Y6L6HEB.js +11 -0
  47. package/dist/echo-VYZW3OTT.js +248 -0
  48. package/dist/export-R4FJ5NOH.js +38 -0
  49. package/dist/history-EVO3L6SC.js +277 -0
  50. package/dist/hooks-MBWE4ILT.js +12 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +568 -0
  53. package/dist/lint-HXKTWRNO.js +316 -0
  54. package/dist/manual-Y3QOXWYA.js +204 -0
  55. package/dist/mcp.js +14745 -0
  56. package/dist/orchestrate-4ZH5GUQH.js +323 -0
  57. package/dist/probe-OYCP4JYG.js +151 -0
  58. package/dist/promote-Z52ZJTJU.js +181 -0
  59. package/dist/providers-4PGPZEWP.js +104 -0
  60. package/dist/remember-6VZ74B7E.js +77 -0
  61. package/dist/ripple-SBQOSTZD.js +215 -0
  62. package/dist/sentinel-LCFD56OJ.js +43 -0
  63. package/dist/server-F5ITNK6T.js +9846 -0
  64. package/dist/server-T6WIFYRQ.js +16076 -0
  65. package/dist/setup-DF4F3ICN.js +25 -0
  66. package/dist/setup-JHBPZAG7.js +296 -0
  67. package/dist/shift-HKIAP4ZN.js +226 -0
  68. package/dist/snapshot-GTVPRYZG.js +62 -0
  69. package/dist/spawn-BJRQA2NR.js +196 -0
  70. package/dist/summary-H6J6N6PJ.js +140 -0
  71. package/dist/switch-6EANJ7O6.js +232 -0
  72. package/dist/sync-BEOCW7TZ.js +11 -0
  73. package/dist/team-NWP2KJAB.js +32 -0
  74. package/dist/test-MA5TWJQV.js +934 -0
  75. package/dist/thread-JCJVRUQR.js +258 -0
  76. package/dist/triage-ETVXXFMV.js +1880 -0
  77. package/dist/tutorial-L5Q3ZDHK.js +666 -0
  78. package/dist/university-R2WDQLSI.js +40 -0
  79. package/dist/upgrade-5B3YGGC6.js +550 -0
  80. package/dist/validate-F3YHBCRZ.js +39 -0
  81. package/dist/validate-QEEY6KFS.js +64 -0
  82. package/dist/watch-4LT4O6K7.js +123 -0
  83. package/dist/watch-6IIWPWDN.js +111 -0
  84. package/dist/wisdom-LRM4FFCH.js +319 -0
  85. package/package.json +68 -0
  86. package/templates/paradigm/config.yaml +175 -0
  87. package/templates/paradigm/docs/commands.md +727 -0
  88. package/templates/paradigm/docs/decisions/000-template.md +47 -0
  89. package/templates/paradigm/docs/decisions/README.md +26 -0
  90. package/templates/paradigm/docs/error-patterns.md +215 -0
  91. package/templates/paradigm/docs/patterns.md +358 -0
  92. package/templates/paradigm/docs/queries.md +200 -0
  93. package/templates/paradigm/docs/troubleshooting.md +477 -0
  94. package/templates/paradigm/echoes.yaml +25 -0
  95. package/templates/paradigm/prompts/add-feature.md +152 -0
  96. package/templates/paradigm/prompts/add-gate.md +117 -0
  97. package/templates/paradigm/prompts/debug-auth.md +174 -0
  98. package/templates/paradigm/prompts/implement-ftux.md +722 -0
  99. package/templates/paradigm/prompts/implement-sandbox.md +651 -0
  100. package/templates/paradigm/prompts/read-docs.md +84 -0
  101. package/templates/paradigm/prompts/refactor.md +106 -0
  102. package/templates/paradigm/prompts/run-e2e-tests.md +340 -0
  103. package/templates/paradigm/prompts/trace-flow.md +202 -0
  104. package/templates/paradigm/prompts/validate-portals.md +279 -0
  105. package/templates/paradigm/specs/context-tracking.md +200 -0
  106. package/templates/paradigm/specs/context.md +461 -0
  107. package/templates/paradigm/specs/disciplines.md +413 -0
  108. package/templates/paradigm/specs/history.md +339 -0
  109. package/templates/paradigm/specs/logger.md +303 -0
  110. package/templates/paradigm/specs/navigator.md +236 -0
  111. package/templates/paradigm/specs/purpose.md +265 -0
  112. package/templates/paradigm/specs/scan.md +177 -0
  113. package/templates/paradigm/specs/symbols.md +451 -0
  114. 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
+ };