@a-company/paradigm 3.1.5 → 3.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 (80) hide show
  1. package/dist/{accept-orchestration-CWZNCGZX.js → accept-orchestration-DIGPJVUR.js} +6 -5
  2. package/dist/{aggregate-W7Q6VIM2.js → aggregate-V4KPR3RW.js} +2 -2
  3. package/dist/{beacon-B47XSTL7.js → beacon-XRXL5KZB.js} +2 -2
  4. package/dist/{chunk-4LGLU2LO.js → chunk-2E2RTBSM.js} +533 -182
  5. package/dist/{chunk-YCLN7WXV.js → chunk-2QNZ6PVD.js} +219 -35
  6. package/dist/{chunk-UM54F7G5.js → chunk-4N6AYEEA.js} +1 -1
  7. package/dist/{chunk-MVXJVRFI.js → chunk-5TUAVVIG.js} +65 -1
  8. package/dist/{chunk-5C4SGQKH.js → chunk-6P4IFIK2.js} +4 -2
  9. package/dist/{chunk-WS5KM7OL.js → chunk-6RNYVBSG.js} +1 -1
  10. package/dist/{chunk-N6PJAPDE.js → chunk-AK5M6KJB.js} +18 -0
  11. package/dist/{chunk-VZ7CXFRZ.js → chunk-CRICL4FQ.js} +1004 -17
  12. package/dist/{chunk-MC7XC7XQ.js → chunk-GZDFVP2N.js} +20 -13
  13. package/dist/chunk-HPC3JAUP.js +42 -0
  14. package/dist/chunk-IRVA7NKV.js +657 -0
  15. package/dist/{chunk-ZPN7MXRA.js → chunk-KFHK6EBI.js} +184 -1
  16. package/dist/{chunk-UUZ2DMG5.js → chunk-KWDTBXP2.js} +1 -1
  17. package/dist/{chunk-DRUDZKIT.js → chunk-M2XMTJHQ.js} +693 -70
  18. package/dist/{chunk-PW2EXJQT.js → chunk-MRENOFTR.js} +24 -1
  19. package/dist/{chunk-QS36NGWV.js → chunk-QHJGB5TV.js} +1 -1
  20. package/dist/chunk-UI3XXVJ6.js +449 -0
  21. package/dist/{chunk-AD2LSCHB.js → chunk-Y4XZWCHK.js} +40 -74
  22. package/dist/{constellation-K3CIQCHI.js → constellation-GNK5DIMH.js} +2 -2
  23. package/dist/{cost-AEK6R7HK.js → cost-AGO5N7DD.js} +1 -1
  24. package/dist/{cursorrules-KI5QWHIX.js → cursorrules-LQFA7M62.js} +2 -2
  25. package/dist/{delete-W67IVTLJ.js → delete-3YXAJ5AA.js} +12 -1
  26. package/dist/{diff-AJJ5H6HV.js → diff-J6C5IHPV.js} +6 -5
  27. package/dist/{dist-2F7NO4H4-KSL6SJIO.js → dist-AG5JNIZU-XSEZ2LLK.js} +28 -3
  28. package/dist/dist-JOHRYQUA.js +7294 -0
  29. package/dist/{dist-NHJQVVUW.js → dist-Q6SAZI7X.js} +2 -2
  30. package/dist/{dist-GPQ4LAY3.js → dist-YP2CO4TG.js} +24 -6
  31. package/dist/{doctor-JBIV5PMN.js → doctor-TQYRF7KK.js} +2 -2
  32. package/dist/{edit-Y7XPYSMK.js → edit-EOMPXOG5.js} +1 -1
  33. package/dist/flow-7JUH6D4H.js +185 -0
  34. package/dist/global-AXILUM5X.js +136 -0
  35. package/dist/{habits-FA65W77Y.js → habits-CHP4EW5H.js} +234 -5
  36. package/dist/{hooks-JKWO44WH.js → hooks-DLZEYHI3.js} +1 -1
  37. package/dist/index.js +125 -100
  38. package/dist/{lint-HXKTWRNO.js → lint-N4LMMEXH.js} +141 -1
  39. package/dist/{list-R3QWW4SC.js → list-JKBJ7ESH.js} +1 -1
  40. package/dist/mcp.js +9273 -6515
  41. package/dist/{orchestrate-4ZH5GUQH.js → orchestrate-FAV64G2R.js} +6 -5
  42. package/dist/{probe-OYCP4JYG.js → probe-X3J2JX62.js} +18 -3
  43. package/dist/{promote-E6NBZ3BK.js → promote-HZH5E5CO.js} +1 -1
  44. package/dist/{providers-4PGPZEWP.js → providers-NQ67LO2Z.js} +1 -1
  45. package/dist/{record-OHQNWOUP.js → record-EECZ3E4I.js} +1 -1
  46. package/dist/{remember-6VZ74B7E.js → remember-3KJZGDUG.js} +1 -1
  47. package/dist/{review-RUHX25A5.js → review-BF26ILZB.js} +1 -1
  48. package/dist/{ripple-SBQOSTZD.js → ripple-JIUAMBLA.js} +2 -2
  49. package/dist/sentinel-ZTL224IG.js +63 -0
  50. package/dist/{server-MV4HNFVF.js → server-MZBYDXJY.js} +4193 -9
  51. package/dist/{setup-DF4F3ICN.js → setup-363IB6MO.js} +1 -1
  52. package/dist/{setup-JHBPZAG7.js → setup-UKJ3VGHI.js} +4 -4
  53. package/dist/{shift-2LQFQP4P.js → shift-KDVYB6CR.js} +16 -13
  54. package/dist/{show-WTOJXUTN.js → show-SAMTXEHG.js} +1 -1
  55. package/dist/{snapshot-GTVPRYZG.js → snapshot-KCMONZAO.js} +2 -2
  56. package/dist/{spawn-BJRQA2NR.js → spawn-EO7B2UM3.js} +2 -2
  57. package/dist/{summary-5SBFO7QK.js → summary-E2PU4UN2.js} +3 -3
  58. package/dist/{switch-6EANJ7O6.js → switch-CC2KACXO.js} +1 -1
  59. package/dist/{sync-5KSTPJ4B.js → sync-5VJPZQNX.js} +2 -2
  60. package/dist/sync-llms-7QDA3ZWC.js +166 -0
  61. package/dist/{team-NWP2KJAB.js → team-6CCNANKE.js} +7 -6
  62. package/dist/{test-MA5TWJQV.js → test-DK2RWLTK.js} +91 -8
  63. package/dist/{thread-JCJVRUQR.js → thread-RNSLADXN.js} +18 -2
  64. package/dist/{timeline-P7BARFLI.js → timeline-TJDVVVA3.js} +1 -1
  65. package/dist/{triage-TBIWJA6R.js → triage-PXMU3RWV.js} +2 -2
  66. package/dist/university-content/courses/para-101.json +2 -1
  67. package/dist/university-content/courses/para-201.json +102 -3
  68. package/dist/university-content/courses/para-301.json +14 -11
  69. package/dist/university-content/courses/para-401.json +57 -3
  70. package/dist/university-content/courses/para-501.json +204 -6
  71. package/dist/university-content/plsat/v3.0.json +808 -3
  72. package/dist/university-content/reference.json +270 -0
  73. package/dist/{upgrade-TIYFQYPO.js → upgrade-RBSE4M6I.js} +1 -1
  74. package/dist/{validate-QEEY6KFS.js → validate-2LTHHORX.js} +1 -1
  75. package/dist/{watch-4LT4O6K7.js → watch-NBPOMOEX.js} +76 -0
  76. package/dist/{watch-2XEYUH43.js → watch-PAEH6MOG.js} +1 -1
  77. package/package.json +1 -1
  78. package/dist/chunk-GWM2WRXL.js +0 -1095
  79. package/dist/sentinel-WB7GIK4V.js +0 -43
  80. /package/dist/{chunk-TAP5N3HH.js → chunk-CCG6KYBT.js} +0 -0
@@ -4,6 +4,7 @@
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import * as os from "os";
7
+ import { execSync } from "child_process";
7
8
  import chalk from "chalk";
8
9
 
9
10
  // src/commands/hooks/generated-hooks.ts
@@ -248,6 +249,8 @@ fi
248
249
  # --- Check 7: Lore entry expected for significant sessions ---
249
250
  if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
250
251
  LORE_RECORDED=false
252
+
253
+ # Check git diff first (covers staged/committed lore)
251
254
  for file in $MODIFIED; do
252
255
  case "$file" in
253
256
  .paradigm/lore/entries/*.yaml|.paradigm/lore/entries/*/*.yaml)
@@ -257,6 +260,17 @@ if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
257
260
  esac
258
261
  done
259
262
 
263
+ # Also check for recent lore on disk (covers MCP-written entries not yet staged)
264
+ if [ "$LORE_RECORDED" = false ]; then
265
+ TODAY=$(date -u +"%Y-%m-%d")
266
+ if [ -d ".paradigm/lore/entries/$TODAY" ]; then
267
+ ENTRY_COUNT=$(find ".paradigm/lore/entries/$TODAY" -name "*.yaml" 2>/dev/null | head -1)
268
+ if [ -n "$ENTRY_COUNT" ]; then
269
+ LORE_RECORDED=true
270
+ fi
271
+ fi
272
+ fi
273
+
260
274
  if [ "$LORE_RECORDED" = false ]; then
261
275
  VIOLATIONS="$VIOLATIONS
262
276
  - You modified $SOURCE_COUNT source files but recorded no lore entry.
@@ -460,6 +474,107 @@ for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index
460
474
  done
461
475
 
462
476
  # Never block \u2014 exit 0
477
+ exit 0
478
+ `;
479
+ var CURSOR_SESSION_START_HOOK = `#!/bin/sh
480
+ # Paradigm Cursor Session Start Hook
481
+ # Fires before the agent does anything \u2014 injects additional_context
482
+ # that acts as a deterministic system prompt (not subject to context compaction).
483
+ # Installed by: paradigm hooks install --cursor
484
+ #
485
+ # Hook type: sessionStart
486
+ # Output: JSON with additional_context + continue: true
487
+ # Exit 0 always (never blocks)
488
+
489
+ # Read JSON from stdin (hook input)
490
+ INPUT=$(cat)
491
+
492
+ # Extract workspace root from Cursor's input
493
+ if command -v jq >/dev/null 2>&1; then
494
+ CWD=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
495
+ else
496
+ CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\["[^"]*"' | head -1 | sed 's/.*\\["//' | sed 's/"$//')
497
+ fi
498
+
499
+ if [ -z "$CWD" ]; then
500
+ CWD="$(pwd)"
501
+ fi
502
+
503
+ # Not a paradigm project \u2014 pass without injection
504
+ if [ ! -d "$CWD/.paradigm" ]; then
505
+ echo '{"continue":true}'
506
+ exit 0
507
+ fi
508
+
509
+ # Build the additional_context payload
510
+ # This is injected as a system-level context that survives context compaction.
511
+
512
+ # Detect project characteristics for task-size guidance
513
+ HAS_PORTAL="false"
514
+ if [ -f "$CWD/portal.yaml" ]; then
515
+ HAS_PORTAL="true"
516
+ fi
517
+
518
+ HAS_LORE="false"
519
+ if [ -d "$CWD/.paradigm/lore" ]; then
520
+ HAS_LORE="true"
521
+ fi
522
+
523
+ HAS_FLOWS="false"
524
+ if [ -f "$CWD/.paradigm/flow-index.json" ]; then
525
+ HAS_FLOWS="true"
526
+ fi
527
+
528
+ # Build context string (escaped for JSON)
529
+ CONTEXT="MANDATORY PARADIGM PROTOCOL \u2014 These rules are NON-NEGOTIABLE:\\\\n\\\\n"
530
+ CONTEXT="\${CONTEXT}Rule 1: SESSION BOOKENDS\\\\n"
531
+ CONTEXT="\${CONTEXT} - FIRST action: Call paradigm_session_recover() to load previous session context\\\\n"
532
+ CONTEXT="\${CONTEXT} - LAST action before finishing: Call paradigm_pm_postflight() with filesModified and symbolsTouched\\\\n\\\\n"
533
+
534
+ CONTEXT="\${CONTEXT}Rule 2: .PURPOSE UPDATES\\\\n"
535
+ CONTEXT="\${CONTEXT} - Every source file you modify MUST have a covering .purpose file\\\\n"
536
+ CONTEXT="\${CONTEXT} - Update the nearest .purpose file with: #components, ~aspects (with anchors), !signals\\\\n"
537
+ CONTEXT="\${CONTEXT} - The STOP HOOK WILL BLOCK you if .purpose files are not updated\\\\n\\\\n"
538
+
539
+ CONTEXT="\${CONTEXT}Rule 3: RIPPLE BEFORE MODIFY\\\\n"
540
+ CONTEXT="\${CONTEXT} - Before modifying any existing symbol, call paradigm_ripple({ symbol: \\\\\\"#symbol-name\\\\\\" })\\\\n"
541
+ CONTEXT="\${CONTEXT} - This shows the blast radius \u2014 what else will break if you change it\\\\n\\\\n"
542
+
543
+ CONTEXT="\${CONTEXT}ESSENTIAL MCP TOOLS:\\\\n"
544
+ CONTEXT="\${CONTEXT} paradigm_session_recover() \u2014 Load previous session (call FIRST)\\\\n"
545
+ CONTEXT="\${CONTEXT} paradigm_ripple({ symbol }) \u2014 Check impact before modifying\\\\n"
546
+ CONTEXT="\${CONTEXT} paradigm_pm_postflight({ filesModified, symbolsTouched }) \u2014 Compliance check (call LAST)\\\\n"
547
+ CONTEXT="\${CONTEXT} paradigm_purpose_add_component({ path, name, description }) \u2014 Register code units\\\\n"
548
+ CONTEXT="\${CONTEXT} paradigm_reindex() \u2014 Rebuild indexes after .purpose changes\\\\n"
549
+ CONTEXT="\${CONTEXT} paradigm_lore_record({ type, title, summary, symbols_touched }) \u2014 Record session\\\\n"
550
+
551
+ if [ "$HAS_PORTAL" = "true" ]; then
552
+ CONTEXT="\${CONTEXT} paradigm_gates_for_route({ method, path }) \u2014 Get auth gate suggestions for endpoints\\\\n"
553
+ CONTEXT="\${CONTEXT} paradigm_portal_add_route({ method, path, gates }) \u2014 Register route with gates\\\\n"
554
+ fi
555
+
556
+ CONTEXT="\${CONTEXT}\\\\nTASK-SIZE TIERS:\\\\n"
557
+ CONTEXT="\${CONTEXT} 1 file: Session bookends only (recover + postflight)\\\\n"
558
+ CONTEXT="\${CONTEXT} 2-3 files: + ripple before modify + update .purpose files\\\\n"
559
+ CONTEXT="\${CONTEXT} 3+ files: + full workflow (ripple, .purpose, lore entry"
560
+
561
+ if [ "$HAS_PORTAL" = "true" ]; then
562
+ CONTEXT="\${CONTEXT}, portal.yaml for routes"
563
+ fi
564
+
565
+ if [ "$HAS_FLOWS" = "true" ]; then
566
+ CONTEXT="\${CONTEXT}, flow validation"
567
+ fi
568
+
569
+ CONTEXT="\${CONTEXT})\\\\n"
570
+
571
+ if [ "$HAS_LORE" = "true" ]; then
572
+ CONTEXT="\${CONTEXT}\\\\nLORE: This project tracks session history. Record a lore entry when modifying 3+ source files.\\\\n"
573
+ fi
574
+
575
+ # Output JSON to stdout
576
+ printf '{"additional_context":"%s","continue":true}\\n' "$CONTEXT"
577
+
463
578
  exit 0
464
579
  `;
465
580
  var CURSOR_STOP_HOOK = `#!/bin/sh
@@ -501,6 +616,26 @@ fi
501
616
 
502
617
  cd "$CWD" || exit 0
503
618
 
619
+ # --- Loop guard: prevent infinite retry loops ---
620
+ # Cursor's stop hook with loop_limit fires repeatedly. Cap retries at 3.
621
+ LOOP_GUARD_FILE=".paradigm/.stop-hook-active"
622
+ if [ -f "$LOOP_GUARD_FILE" ]; then
623
+ RETRY_COUNT=$(cat "$LOOP_GUARD_FILE" 2>/dev/null | tr -d '[:space:]')
624
+ RETRY_COUNT=\${RETRY_COUNT:-0}
625
+ if [ "$RETRY_COUNT" -ge 3 ]; then
626
+ # Max retries reached \u2014 allow session to end to avoid infinite loop
627
+ echo "[paradigm] Stop hook: max retries (3) reached. Allowing session to end." >&2
628
+ rm -f "$LOOP_GUARD_FILE"
629
+ rm -f ".paradigm/.pending-review"
630
+ rm -f ".paradigm/.habits-blocking"
631
+ exit 0
632
+ fi
633
+ RETRY_COUNT=$((RETRY_COUNT + 1))
634
+ echo "$RETRY_COUNT" > "$LOOP_GUARD_FILE"
635
+ else
636
+ echo "1" > "$LOOP_GUARD_FILE"
637
+ fi
638
+
504
639
  # Get modified files (uncommitted changes)
505
640
  MODIFIED=$(git diff --name-only HEAD 2>/dev/null)
506
641
  if [ -z "$MODIFIED" ]; then
@@ -703,6 +838,8 @@ fi
703
838
  # --- Check 7: Lore entry expected for significant sessions ---
704
839
  if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
705
840
  LORE_RECORDED=false
841
+
842
+ # Check git diff first (covers staged/committed lore)
706
843
  for file in $MODIFIED; do
707
844
  case "$file" in
708
845
  .paradigm/lore/entries/*.yaml|.paradigm/lore/entries/*/*.yaml)
@@ -712,6 +849,17 @@ if [ "$SOURCE_COUNT" -ge 3 ] && [ -d ".paradigm/lore" ]; then
712
849
  esac
713
850
  done
714
851
 
852
+ # Also check for recent lore on disk (covers MCP-written entries not yet staged)
853
+ if [ "$LORE_RECORDED" = false ]; then
854
+ TODAY=$(date -u +"%Y-%m-%d")
855
+ if [ -d ".paradigm/lore/entries/$TODAY" ]; then
856
+ ENTRY_COUNT=$(find ".paradigm/lore/entries/$TODAY" -name "*.yaml" 2>/dev/null | head -1)
857
+ if [ -n "$ENTRY_COUNT" ]; then
858
+ LORE_RECORDED=true
859
+ fi
860
+ fi
861
+ fi
862
+
715
863
  if [ "$LORE_RECORDED" = false ]; then
716
864
  VIOLATIONS="$VIOLATIONS
717
865
  - You modified $SOURCE_COUNT source files but recorded no lore entry.
@@ -756,6 +904,13 @@ if [ "$VIOLATION_COUNT" -gt 0 ]; then
756
904
  echo " 4. paradigm_reindex \u2014 rebuild indexes after updates" >&2
757
905
  echo " 5. paradigm_lore_record \u2014 record session lore entry" >&2
758
906
  echo " 6. paradigm_habits_check \u2014 evaluate habit compliance" >&2
907
+
908
+ # Output followup_message JSON to stdout for Cursor's compliance loop.
909
+ # Cursor auto-submits this as the next user message, creating a retry loop.
910
+ # Escape violations for JSON embedding (newlines \u2192 \\n, quotes \u2192 \\", backslash \u2192 \\\\)
911
+ ESCAPED_VIOLATIONS=$(printf '%s' "$VIOLATIONS" | sed 's/\\\\/\\\\\\\\/g' | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\\n/\\\\n/g')
912
+ printf '{"followup_message":"Paradigm compliance check found %d violation(s). Fix these:\\\\n%s\\\\nThen try finishing again."}\\n' "$VIOLATION_COUNT" "$ESCAPED_VIOLATIONS"
913
+
759
914
  exit 2
760
915
  fi
761
916
 
@@ -766,19 +921,25 @@ if [ -n "$ADVISORY" ]; then
766
921
  echo "$ADVISORY" >&2
767
922
  fi
768
923
 
769
- # Clean up pending-review on pass
924
+ # Clean up pending-review and loop guard on pass
770
925
  rm -f ".paradigm/.pending-review"
771
926
  rm -f ".paradigm/.habits-blocking"
927
+ rm -f ".paradigm/.stop-hook-active"
772
928
 
773
929
  exit 0
774
930
  `;
775
931
  var CURSOR_POSTWRITE_HOOK = `#!/bin/sh
776
- # Paradigm Cursor PostWrite Hook (v2)
777
- # Fires after file edits.
778
- # Tracks modified source files in .paradigm/.pending-review
779
- # and outputs compliance reminders.
932
+ # Paradigm Cursor PostWrite Hook (v2) \u2014 LEGACY
933
+ # Fires after file edits via Cursor's afterFileEdit hook type.
780
934
  # Installed by: paradigm hooks install --cursor
781
935
  #
936
+ # IMPORTANT: Cursor ignores all output (stdout + stderr) from afterFileEdit hooks.
937
+ # This hook's advisory messages are INVISIBLE to the agent. The postToolUse hook
938
+ # (cursor-posttooluse.sh) is now the primary advisory mechanism.
939
+ #
940
+ # This hook is kept for backward compatibility and background file tracking only.
941
+ # Both preToolUse and stop hooks depend on the .pending-review file this writes.
942
+ #
782
943
  # Hook type: afterFileEdit
783
944
  # Exit 0 always (never blocks \u2014 advisory only)
784
945
 
@@ -856,18 +1017,8 @@ if [ -z "$found_purpose" ] && [ -f ".purpose" ]; then
856
1017
  found_purpose=".purpose"
857
1018
  fi
858
1019
 
859
- if [ -z "$found_purpose" ]; then
860
- file_dir=$(dirname "$REL_PATH")
861
- echo "" >&2
862
- echo "[paradigm] No .purpose file covers $file_dir/" >&2
863
- echo " Create one: paradigm_purpose_init + paradigm_purpose_add_component" >&2
864
- echo " $PENDING_COUNT file(s) pending review. The stop hook WILL BLOCK." >&2
865
- elif [ "$PENDING_COUNT" -gt 0 ] && [ "$((PENDING_COUNT % 3))" -eq 0 ]; then
866
- echo "" >&2
867
- echo "[paradigm] $PENDING_COUNT source file(s) modified. Update $found_purpose:" >&2
868
- echo " -> #components, ~aspects (with anchors), !signals, \\$flows, ^gates" >&2
869
- echo " The stop hook WILL BLOCK if .purpose files aren't updated." >&2
870
- fi
1020
+ # NOTE: No stderr output here \u2014 Cursor ignores afterFileEdit output.
1021
+ # Advisory messages are handled by cursor-posttooluse.sh (postToolUse hook).
871
1022
 
872
1023
  exit 0
873
1024
  `;
@@ -915,6 +1066,263 @@ for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index
915
1066
  done
916
1067
 
917
1068
  # Never block \u2014 exit 0
1069
+ exit 0
1070
+ `;
1071
+ var CURSOR_PRETOOLUSE_HOOK = `#!/bin/sh
1072
+ # Paradigm Cursor PreToolUse Hook \u2014 Graduated Blocking
1073
+ # Fires BEFORE the agent calls Edit or Write.
1074
+ # Uses graduated enforcement based on uncovered source edits.
1075
+ # Installed by: paradigm hooks install --cursor
1076
+ #
1077
+ # Hook type: preToolUse
1078
+ # Matcher: Edit|Write
1079
+ # Exit 0 = allow, Exit 2 = block with stderr message
1080
+ #
1081
+ # Graduated enforcement:
1082
+ # 1-2 uncovered edits \u2192 silent pass (exit 0)
1083
+ # 3-4 uncovered edits \u2192 warn via stderr (exit 0)
1084
+ # 5+ uncovered edits \u2192 BLOCK (exit 2 + stderr)
1085
+
1086
+ # Read JSON from stdin (hook input)
1087
+ INPUT=$(cat)
1088
+
1089
+ # Extract tool_name and file_path from preToolUse input
1090
+ if command -v jq >/dev/null 2>&1; then
1091
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
1092
+ FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // .input.file_path // empty' 2>/dev/null)
1093
+ else
1094
+ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"tool_name"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
1095
+ FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
1096
+ fi
1097
+
1098
+ # Must have a file path to check
1099
+ if [ -z "$FILE_PATH" ]; then
1100
+ exit 0
1101
+ fi
1102
+
1103
+ # Extract workspace root
1104
+ if command -v jq >/dev/null 2>&1; then
1105
+ CWD=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
1106
+ else
1107
+ CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\["[^"]*"' | head -1 | sed 's/.*\\["//' | sed 's/"$//')
1108
+ fi
1109
+
1110
+ if [ -z "$CWD" ]; then
1111
+ CWD="$(pwd)"
1112
+ fi
1113
+
1114
+ # Not a paradigm project \u2014 pass
1115
+ if [ ! -d "$CWD/.paradigm" ]; then
1116
+ exit 0
1117
+ fi
1118
+
1119
+ cd "$CWD" || exit 0
1120
+
1121
+ # Convert to relative path
1122
+ REL_PATH="$FILE_PATH"
1123
+ case "$FILE_PATH" in
1124
+ "$CWD"/*) REL_PATH=$(echo "$FILE_PATH" | sed "s|^$CWD/||") ;;
1125
+ esac
1126
+
1127
+ # If still absolute, file is outside project \u2014 skip
1128
+ case "$REL_PATH" in
1129
+ /*) exit 0 ;;
1130
+ esac
1131
+
1132
+ # Skip non-source files (paradigm metadata, docs, config)
1133
+ case "$REL_PATH" in
1134
+ *.purpose|portal.yaml|*.md|*.lock|*.log|*.json|*.yaml|*.yml|.gitignore|.env*) exit 0 ;;
1135
+ esac
1136
+
1137
+ # Skip .paradigm, .claude, and .cursor directories
1138
+ case "$REL_PATH" in
1139
+ .paradigm/*|.claude/*|.cursor/*) exit 0 ;;
1140
+ esac
1141
+
1142
+ # Check if target file has a covering .purpose file
1143
+ dir=$(dirname "$REL_PATH")
1144
+ has_purpose=false
1145
+
1146
+ while [ "$dir" != "." ] && [ "$dir" != "/" ] && [ "$dir" != "" ]; do
1147
+ if [ -f "$dir/.purpose" ]; then
1148
+ has_purpose=true
1149
+ break
1150
+ fi
1151
+ dir=$(dirname "$dir")
1152
+ done
1153
+
1154
+ # Check root .purpose
1155
+ if [ "$has_purpose" = false ] && [ -f ".purpose" ]; then
1156
+ has_purpose=true
1157
+ fi
1158
+
1159
+ # If this file already has .purpose coverage, always allow
1160
+ if [ "$has_purpose" = true ]; then
1161
+ exit 0
1162
+ fi
1163
+
1164
+ # Count uncovered source edits from .pending-review
1165
+ PENDING_FILE=".paradigm/.pending-review"
1166
+ UNCOVERED_COUNT=0
1167
+
1168
+ if [ -f "$PENDING_FILE" ]; then
1169
+ while IFS= read -r tracked_file; do
1170
+ [ -z "$tracked_file" ] && continue
1171
+ # Check if this tracked file has .purpose coverage
1172
+ check_dir=$(dirname "$tracked_file")
1173
+ found=false
1174
+ while [ "$check_dir" != "." ] && [ "$check_dir" != "/" ] && [ "$check_dir" != "" ]; do
1175
+ if [ -f "$check_dir/.purpose" ]; then
1176
+ found=true
1177
+ break
1178
+ fi
1179
+ check_dir=$(dirname "$check_dir")
1180
+ done
1181
+ if [ "$found" = false ] && [ -f ".purpose" ]; then
1182
+ found=true
1183
+ fi
1184
+ if [ "$found" = false ]; then
1185
+ UNCOVERED_COUNT=$((UNCOVERED_COUNT + 1))
1186
+ fi
1187
+ done < "$PENDING_FILE"
1188
+ fi
1189
+
1190
+ # Include the current file (not yet tracked)
1191
+ UNCOVERED_COUNT=$((UNCOVERED_COUNT + 1))
1192
+
1193
+ # Graduated enforcement
1194
+ if [ "$UNCOVERED_COUNT" -le 2 ]; then
1195
+ # Silent pass \u2014 don't slow down small fixes
1196
+ exit 0
1197
+ elif [ "$UNCOVERED_COUNT" -le 4 ]; then
1198
+ # Warn but allow
1199
+ echo "" >&2
1200
+ echo "[paradigm] Warning: $UNCOVERED_COUNT source files modified without .purpose coverage." >&2
1201
+ echo " Update the nearest .purpose file before the stop hook blocks you." >&2
1202
+ echo " Use: paradigm_purpose_init + paradigm_purpose_add_component" >&2
1203
+ exit 0
1204
+ else
1205
+ # Block \u2014 too many uncovered edits
1206
+ echo "" >&2
1207
+ echo "[paradigm] BLOCKED: $UNCOVERED_COUNT source files modified without .purpose coverage." >&2
1208
+ echo " You must update .purpose files before continuing." >&2
1209
+ echo " Steps:" >&2
1210
+ echo " 1. paradigm_purpose_init \u2014 create .purpose in uncovered directories" >&2
1211
+ echo " 2. paradigm_purpose_add_component \u2014 register code units" >&2
1212
+ echo " 3. paradigm_reindex \u2014 rebuild the index" >&2
1213
+ echo " Then retry your edit." >&2
1214
+ exit 2
1215
+ fi
1216
+ `;
1217
+ var CURSOR_POSTTOOLUSE_HOOK = `#!/bin/sh
1218
+ # Paradigm Cursor PostToolUse Hook \u2014 Advisory Feedback
1219
+ # Fires AFTER the agent calls Edit or Write.
1220
+ # Tracks modified source files and outputs advisory the agent can see.
1221
+ # Installed by: paradigm hooks install --cursor
1222
+ #
1223
+ # Hook type: postToolUse
1224
+ # Matcher: Edit|Write
1225
+ # Exit 0 always (never blocks \u2014 advisory only)
1226
+ #
1227
+ # Unlike afterFileEdit, postToolUse output is visible to the Cursor agent.
1228
+ # This is the primary advisory mechanism for Cursor enforcement.
1229
+
1230
+ # Read JSON from stdin (hook input)
1231
+ INPUT=$(cat)
1232
+
1233
+ # Extract file_path from postToolUse input
1234
+ if command -v jq >/dev/null 2>&1; then
1235
+ FILE_PATH=$(echo "$INPUT" | jq -r '.file_path // .input.file_path // empty' 2>/dev/null)
1236
+ else
1237
+ FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//')
1238
+ fi
1239
+
1240
+ if [ -z "$FILE_PATH" ]; then
1241
+ exit 0
1242
+ fi
1243
+
1244
+ # Extract workspace root
1245
+ if command -v jq >/dev/null 2>&1; then
1246
+ CWD=$(echo "$INPUT" | jq -r '.workspace_roots[0] // empty' 2>/dev/null)
1247
+ else
1248
+ CWD=$(echo "$INPUT" | grep -o '"workspace_roots"[[:space:]]*:[[:space:]]*\\["[^"]*"' | head -1 | sed 's/.*\\["//' | sed 's/"$//')
1249
+ fi
1250
+
1251
+ if [ -z "$CWD" ]; then
1252
+ CWD="$(pwd)"
1253
+ fi
1254
+
1255
+ # Not a paradigm project \u2014 pass
1256
+ if [ ! -d "$CWD/.paradigm" ]; then
1257
+ exit 0
1258
+ fi
1259
+
1260
+ cd "$CWD" || exit 0
1261
+
1262
+ # Convert to relative path
1263
+ REL_PATH="$FILE_PATH"
1264
+ case "$FILE_PATH" in
1265
+ "$CWD"/*) REL_PATH=$(echo "$FILE_PATH" | sed "s|^$CWD/||") ;;
1266
+ esac
1267
+
1268
+ # If still absolute, file is outside project \u2014 skip
1269
+ case "$REL_PATH" in
1270
+ /*) exit 0 ;;
1271
+ esac
1272
+
1273
+ # Skip non-source files
1274
+ case "$REL_PATH" in
1275
+ *.purpose|portal.yaml|*.md|*.lock|*.log|*.json|*.yaml|*.yml|.gitignore|.env*) exit 0 ;;
1276
+ esac
1277
+
1278
+ # Skip .paradigm, .claude, and .cursor directories
1279
+ case "$REL_PATH" in
1280
+ .paradigm/*|.claude/*|.cursor/*) exit 0 ;;
1281
+ esac
1282
+
1283
+ # Track: append to .paradigm/.pending-review (deduplicated)
1284
+ PENDING_FILE=".paradigm/.pending-review"
1285
+ if [ -f "$PENDING_FILE" ]; then
1286
+ if ! grep -qxF "$REL_PATH" "$PENDING_FILE" 2>/dev/null; then
1287
+ echo "$REL_PATH" >> "$PENDING_FILE"
1288
+ fi
1289
+ else
1290
+ echo "$REL_PATH" > "$PENDING_FILE"
1291
+ fi
1292
+
1293
+ # Count pending files
1294
+ PENDING_COUNT=$(wc -l < "$PENDING_FILE" | tr -d ' ')
1295
+
1296
+ # Walk up from the file's directory to find a .purpose file
1297
+ dir=$(dirname "$REL_PATH")
1298
+ found_purpose=""
1299
+
1300
+ while [ "$dir" != "." ] && [ "$dir" != "/" ] && [ "$dir" != "" ]; do
1301
+ if [ -f "$dir/.purpose" ]; then
1302
+ found_purpose="$dir/.purpose"
1303
+ break
1304
+ fi
1305
+ dir=$(dirname "$dir")
1306
+ done
1307
+
1308
+ # Check root .purpose
1309
+ if [ -z "$found_purpose" ] && [ -f ".purpose" ]; then
1310
+ found_purpose=".purpose"
1311
+ fi
1312
+
1313
+ if [ -z "$found_purpose" ]; then
1314
+ file_dir=$(dirname "$REL_PATH")
1315
+ echo "" >&2
1316
+ echo "[paradigm] No .purpose file covers $file_dir/" >&2
1317
+ echo " Create one: paradigm_purpose_init + paradigm_purpose_add_component" >&2
1318
+ echo " $PENDING_COUNT file(s) pending review. The stop hook WILL BLOCK." >&2
1319
+ elif [ "$PENDING_COUNT" -gt 0 ] && [ "$((PENDING_COUNT % 3))" -eq 0 ]; then
1320
+ echo "" >&2
1321
+ echo "[paradigm] $PENDING_COUNT source file(s) modified. Update $found_purpose:" >&2
1322
+ echo " -> #components, ~aspects (with anchors), !signals, \\$flows, ^gates" >&2
1323
+ echo " The stop hook WILL BLOCK if .purpose files aren't updated." >&2
1324
+ fi
1325
+
918
1326
  exit 0
919
1327
  `;
920
1328
 
@@ -938,6 +1346,68 @@ function isParadigmPluginActive() {
938
1346
  return { active: false };
939
1347
  }
940
1348
  }
1349
+ function checkPluginVersionCompatibility() {
1350
+ try {
1351
+ const pluginInfo = isParadigmPluginActive();
1352
+ if (!pluginInfo.active || !pluginInfo.cacheVersion) {
1353
+ return { compatible: true };
1354
+ }
1355
+ const hooksJsonPath = path.join(
1356
+ os.homedir(),
1357
+ ".claude",
1358
+ "plugins",
1359
+ "cache",
1360
+ "a-paradigm",
1361
+ "paradigm",
1362
+ pluginInfo.cacheVersion,
1363
+ "hooks.json"
1364
+ );
1365
+ if (!fs.existsSync(hooksJsonPath)) {
1366
+ return { compatible: true };
1367
+ }
1368
+ const hooksData = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
1369
+ const compatibleVersions = hooksData.compatibleVersions;
1370
+ if (!compatibleVersions) {
1371
+ return { compatible: true };
1372
+ }
1373
+ const currentVersion = getCurrentParadigmVersion();
1374
+ if (!currentVersion) {
1375
+ return { compatible: true };
1376
+ }
1377
+ const parts = compatibleVersions.split(/\s+/);
1378
+ for (const part of parts) {
1379
+ const match = part.match(/^(>=?|<=?)\s*(\d+\.\d+\.\d+)/);
1380
+ if (!match) continue;
1381
+ const [, op, ver] = match;
1382
+ const cmp = compareVersions(currentVersion, ver);
1383
+ if (op === ">=" && cmp < 0) return { compatible: false, message: `Plugin requires paradigm ${compatibleVersions}, current: ${currentVersion}` };
1384
+ if (op === ">" && cmp <= 0) return { compatible: false, message: `Plugin requires paradigm ${compatibleVersions}, current: ${currentVersion}` };
1385
+ if (op === "<=" && cmp > 0) return { compatible: false, message: `Plugin requires paradigm ${compatibleVersions}, current: ${currentVersion}` };
1386
+ if (op === "<" && cmp >= 0) return { compatible: false, message: `Plugin requires paradigm ${compatibleVersions}, current: ${currentVersion}` };
1387
+ }
1388
+ return { compatible: true };
1389
+ } catch {
1390
+ return { compatible: true };
1391
+ }
1392
+ }
1393
+ function getCurrentParadigmVersion() {
1394
+ try {
1395
+ const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "package.json");
1396
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1397
+ return pkg.version || null;
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ }
1402
+ function compareVersions(a, b) {
1403
+ const pa = a.split(".").map(Number);
1404
+ const pb = b.split(".").map(Number);
1405
+ for (let i = 0; i < 3; i++) {
1406
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
1407
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
1408
+ }
1409
+ return 0;
1410
+ }
941
1411
  function cleanupProjectClaudeCodeHooks(rootDir) {
942
1412
  const removed = [];
943
1413
  const claudeHooksDir = path.join(rootDir, ".claude", "hooks");
@@ -993,6 +1463,25 @@ function cleanupProjectClaudeCodeHooks(rootDir) {
993
1463
  }
994
1464
  return { cleaned: removed.length > 0, removed };
995
1465
  }
1466
+ function validateBashSyntax(scriptContent, scriptName) {
1467
+ try {
1468
+ const tmpPath = path.join(os.tmpdir(), `paradigm-hook-validate-${Date.now()}.sh`);
1469
+ fs.writeFileSync(tmpPath, scriptContent, "utf8");
1470
+ try {
1471
+ execSync(`bash -n "${tmpPath}" 2>&1`, { encoding: "utf-8" });
1472
+ return null;
1473
+ } catch (err) {
1474
+ return `${scriptName}: bash syntax error \u2014 ${err.message?.split("\n")[0] || "unknown error"}`;
1475
+ } finally {
1476
+ try {
1477
+ fs.unlinkSync(tmpPath);
1478
+ } catch {
1479
+ }
1480
+ }
1481
+ } catch {
1482
+ return null;
1483
+ }
1484
+ }
996
1485
  var POST_COMMIT_HOOK = `#!/bin/sh
997
1486
  # Paradigm post-commit hook - captures history from commits
998
1487
  # Installed by: paradigm hooks install
@@ -1089,8 +1578,41 @@ fi
1089
1578
  `;
1090
1579
  async function hooksInstallCommand(options = {}) {
1091
1580
  const rootDir = process.cwd();
1581
+ const dryRun = options.dryRun || false;
1582
+ if (dryRun) {
1583
+ console.log(chalk.cyan("\n [dry-run] Showing what would be installed:\n"));
1584
+ }
1092
1585
  const onlyClaudeCode = options.claudeCode && !options.postCommit && !options.prePush && !options.cursor;
1093
1586
  const onlyCursor = options.cursor && !options.postCommit && !options.prePush && !options.claudeCode;
1587
+ if (!dryRun) {
1588
+ const scriptsToValidate = [
1589
+ { name: "post-commit", content: POST_COMMIT_HOOK },
1590
+ { name: "pre-push", content: PRE_PUSH_HOOK },
1591
+ { name: "claude-code-stop", content: CLAUDE_CODE_STOP_HOOK },
1592
+ { name: "claude-code-precommit", content: CLAUDE_CODE_PRECOMMIT_HOOK },
1593
+ { name: "claude-code-postwrite", content: CLAUDE_CODE_POSTWRITE_HOOK },
1594
+ { name: "cursor-session-start", content: CURSOR_SESSION_START_HOOK },
1595
+ { name: "cursor-stop", content: CURSOR_STOP_HOOK },
1596
+ { name: "cursor-precommit", content: CURSOR_PRECOMMIT_HOOK },
1597
+ { name: "cursor-postwrite", content: CURSOR_POSTWRITE_HOOK },
1598
+ { name: "cursor-pretooluse", content: CURSOR_PRETOOLUSE_HOOK },
1599
+ { name: "cursor-posttooluse", content: CURSOR_POSTTOOLUSE_HOOK }
1600
+ ];
1601
+ for (const script of scriptsToValidate) {
1602
+ const err = validateBashSyntax(script.content, script.name);
1603
+ if (err) {
1604
+ console.log(chalk.red(`Hook syntax error: ${err}`));
1605
+ console.log(chalk.gray("Aborting installation. Fix the hook script and try again."));
1606
+ return;
1607
+ }
1608
+ }
1609
+ }
1610
+ const compat = checkPluginVersionCompatibility();
1611
+ if (!compat.compatible) {
1612
+ console.log(chalk.yellow(`
1613
+ \u26A0 ${compat.message}`));
1614
+ console.log(chalk.gray(" Hook installation will continue, but behavior may differ from plugin expectations.\n"));
1615
+ }
1094
1616
  if (!onlyClaudeCode && !onlyCursor) {
1095
1617
  const gitDir = path.join(rootDir, ".git");
1096
1618
  if (!fs.existsSync(gitDir)) {
@@ -1098,53 +1620,79 @@ async function hooksInstallCommand(options = {}) {
1098
1620
  return;
1099
1621
  }
1100
1622
  const hooksDir = path.join(gitDir, "hooks");
1101
- fs.mkdirSync(hooksDir, { recursive: true });
1102
- const installAll2 = !options.postCommit && !options.prePush && !options.claudeCode;
1623
+ const installAllGit = !options.postCommit && !options.prePush && !options.claudeCode;
1103
1624
  const installed = [];
1104
- if (installAll2 || options.postCommit) {
1625
+ if (installAllGit || options.postCommit) {
1105
1626
  const hookPath = path.join(hooksDir, "post-commit");
1106
- if (fs.existsSync(hookPath) && !options.force) {
1107
- const content = fs.readFileSync(hookPath, "utf8");
1108
- if (!content.includes("paradigm")) {
1109
- console.log(chalk.yellow("post-commit hook exists. Use --force to overwrite."));
1627
+ if (dryRun) {
1628
+ const action = fs.existsSync(hookPath) && !options.force ? "skip (exists)" : "install";
1629
+ console.log(chalk.gray(` post-commit: ${action} \u2192 ${hookPath}`));
1630
+ } else {
1631
+ if (fs.existsSync(hookPath) && !options.force) {
1632
+ const content = fs.readFileSync(hookPath, "utf8");
1633
+ if (!content.includes("paradigm")) {
1634
+ console.log(chalk.yellow("post-commit hook exists. Use --force to overwrite."));
1635
+ } else {
1636
+ console.log(chalk.gray("post-commit hook already installed by paradigm"));
1637
+ }
1110
1638
  } else {
1111
- console.log(chalk.gray("post-commit hook already installed by paradigm"));
1639
+ fs.mkdirSync(hooksDir, { recursive: true });
1640
+ fs.writeFileSync(hookPath, POST_COMMIT_HOOK);
1641
+ fs.chmodSync(hookPath, "755");
1642
+ installed.push("post-commit");
1112
1643
  }
1113
- } else {
1114
- fs.writeFileSync(hookPath, POST_COMMIT_HOOK);
1115
- fs.chmodSync(hookPath, "755");
1116
- installed.push("post-commit");
1117
1644
  }
1118
1645
  }
1119
- if (installAll2 || options.prePush) {
1646
+ if (installAllGit || options.prePush) {
1120
1647
  const hookPath = path.join(hooksDir, "pre-push");
1121
- if (fs.existsSync(hookPath) && !options.force) {
1122
- const content = fs.readFileSync(hookPath, "utf8");
1123
- if (!content.includes("paradigm")) {
1124
- console.log(chalk.yellow("pre-push hook exists. Use --force to overwrite."));
1648
+ if (dryRun) {
1649
+ const action = fs.existsSync(hookPath) && !options.force ? "skip (exists)" : "install";
1650
+ console.log(chalk.gray(` pre-push: ${action} \u2192 ${hookPath}`));
1651
+ } else {
1652
+ if (fs.existsSync(hookPath) && !options.force) {
1653
+ const content = fs.readFileSync(hookPath, "utf8");
1654
+ if (!content.includes("paradigm")) {
1655
+ console.log(chalk.yellow("pre-push hook exists. Use --force to overwrite."));
1656
+ } else {
1657
+ console.log(chalk.gray("pre-push hook already installed by paradigm"));
1658
+ }
1125
1659
  } else {
1126
- console.log(chalk.gray("pre-push hook already installed by paradigm"));
1660
+ fs.mkdirSync(hooksDir, { recursive: true });
1661
+ fs.writeFileSync(hookPath, PRE_PUSH_HOOK);
1662
+ fs.chmodSync(hookPath, "755");
1663
+ installed.push("pre-push");
1127
1664
  }
1128
- } else {
1129
- fs.writeFileSync(hookPath, PRE_PUSH_HOOK);
1130
- fs.chmodSync(hookPath, "755");
1131
- installed.push("pre-push");
1132
1665
  }
1133
1666
  }
1134
- if (installed.length > 0) {
1667
+ if (!dryRun && installed.length > 0) {
1135
1668
  console.log(chalk.green(`Git hooks installed: ${installed.join(", ")}`));
1136
1669
  }
1137
1670
  const historyDir = path.join(rootDir, ".paradigm/history");
1138
- if (!fs.existsSync(historyDir)) {
1671
+ if (!fs.existsSync(historyDir) && !dryRun) {
1139
1672
  console.log(chalk.gray("Tip: Run `paradigm history init` to initialize history tracking"));
1140
1673
  }
1141
1674
  }
1142
1675
  const installAll = !options.postCommit && !options.prePush && !options.claudeCode && !options.cursor;
1143
1676
  if (installAll || options.claudeCode) {
1144
- await installClaudeCodeHooks(rootDir, options.force);
1677
+ if (dryRun) {
1678
+ console.log(chalk.gray(" Claude Code hooks: would install paradigm-stop.sh, paradigm-precommit.sh, paradigm-postwrite.sh"));
1679
+ console.log(chalk.gray(` \u2192 ${path.join(rootDir, ".claude", "hooks")}/`));
1680
+ console.log(chalk.gray(" \u2192 Update .claude/settings.json with hook configuration"));
1681
+ } else {
1682
+ await installClaudeCodeHooks(rootDir, options.force);
1683
+ }
1145
1684
  }
1146
1685
  if (installAll || options.cursor) {
1147
- await installCursorHooks(rootDir, options.force);
1686
+ if (dryRun) {
1687
+ console.log(chalk.gray(" Cursor hooks: would install paradigm-session-start.sh, paradigm-stop.sh, paradigm-precommit.sh, paradigm-postwrite.sh, paradigm-pretooluse.sh, paradigm-posttooluse.sh"));
1688
+ console.log(chalk.gray(` \u2192 ${path.join(rootDir, ".cursor", "hooks")}/`));
1689
+ console.log(chalk.gray(" \u2192 Update .cursor/hooks.json"));
1690
+ } else {
1691
+ await installCursorHooks(rootDir, options.force);
1692
+ }
1693
+ }
1694
+ if (dryRun) {
1695
+ console.log(chalk.cyan("\n [dry-run] No changes made.\n"));
1148
1696
  }
1149
1697
  }
1150
1698
  async function installClaudeCodeHooks(rootDir, force) {
@@ -1246,9 +1794,12 @@ async function installCursorHooks(rootDir, force) {
1246
1794
  fs.mkdirSync(cursorHooksDir, { recursive: true });
1247
1795
  const installed = [];
1248
1796
  const hookScripts = [
1797
+ { name: "paradigm-session-start.sh", content: CURSOR_SESSION_START_HOOK },
1249
1798
  { name: "paradigm-stop.sh", content: CURSOR_STOP_HOOK },
1250
1799
  { name: "paradigm-precommit.sh", content: CURSOR_PRECOMMIT_HOOK },
1251
- { name: "paradigm-postwrite.sh", content: CURSOR_POSTWRITE_HOOK }
1800
+ { name: "paradigm-postwrite.sh", content: CURSOR_POSTWRITE_HOOK },
1801
+ { name: "paradigm-pretooluse.sh", content: CURSOR_PRETOOLUSE_HOOK },
1802
+ { name: "paradigm-posttooluse.sh", content: CURSOR_POSTTOOLUSE_HOOK }
1252
1803
  ];
1253
1804
  for (const hook of hookScripts) {
1254
1805
  const destPath = path.join(cursorHooksDir, hook.name);
@@ -1270,9 +1821,14 @@ async function installCursorHooks(rootDir, force) {
1270
1821
  }
1271
1822
  hooksConfig.version = 1;
1272
1823
  const hooks = hooksConfig.hooks || {};
1824
+ const paradigmSessionStartEntry = {
1825
+ command: ".cursor/hooks/paradigm-session-start.sh",
1826
+ timeout: 5
1827
+ };
1273
1828
  const paradigmStopEntry = {
1274
1829
  command: ".cursor/hooks/paradigm-stop.sh",
1275
- timeout: 10
1830
+ timeout: 10,
1831
+ loop_limit: 3
1276
1832
  };
1277
1833
  const paradigmPostwriteEntry = {
1278
1834
  command: ".cursor/hooks/paradigm-postwrite.sh",
@@ -1283,6 +1839,14 @@ async function installCursorHooks(rootDir, force) {
1283
1839
  matcher: "git commit",
1284
1840
  timeout: 30
1285
1841
  };
1842
+ const sessionStartHooks = hooks.sessionStart || [];
1843
+ const hasParadigmSessionStart = sessionStartHooks.some(
1844
+ (h) => JSON.stringify(h).includes("paradigm-session-start.sh")
1845
+ );
1846
+ if (!hasParadigmSessionStart) {
1847
+ sessionStartHooks.push(paradigmSessionStartEntry);
1848
+ }
1849
+ hooks.sessionStart = sessionStartHooks;
1286
1850
  const stopHooks = hooks.stop || [];
1287
1851
  const hasParadigmStop = stopHooks.some(
1288
1852
  (h) => JSON.stringify(h).includes("paradigm-stop.sh")
@@ -1299,6 +1863,32 @@ async function installCursorHooks(rootDir, force) {
1299
1863
  afterFileEditHooks.push(paradigmPostwriteEntry);
1300
1864
  }
1301
1865
  hooks.afterFileEdit = afterFileEditHooks;
1866
+ const paradigmPretoolUseEntry = {
1867
+ command: ".cursor/hooks/paradigm-pretooluse.sh",
1868
+ matcher: "Edit|Write",
1869
+ timeout: 5
1870
+ };
1871
+ const preToolUseHooks = hooks.preToolUse || [];
1872
+ const hasParadigmPretoolUse = preToolUseHooks.some(
1873
+ (h) => JSON.stringify(h).includes("paradigm-pretooluse.sh")
1874
+ );
1875
+ if (!hasParadigmPretoolUse) {
1876
+ preToolUseHooks.push(paradigmPretoolUseEntry);
1877
+ }
1878
+ hooks.preToolUse = preToolUseHooks;
1879
+ const paradigmPosttoolUseEntry = {
1880
+ command: ".cursor/hooks/paradigm-posttooluse.sh",
1881
+ matcher: "Edit|Write",
1882
+ timeout: 5
1883
+ };
1884
+ const postToolUseHooks = hooks.postToolUse || [];
1885
+ const hasParadigmPosttoolUse = postToolUseHooks.some(
1886
+ (h) => JSON.stringify(h).includes("paradigm-posttooluse.sh")
1887
+ );
1888
+ if (!hasParadigmPosttoolUse) {
1889
+ postToolUseHooks.push(paradigmPosttoolUseEntry);
1890
+ }
1891
+ hooks.postToolUse = postToolUseHooks;
1302
1892
  const beforeShellHooks = hooks.beforeShellExecution || [];
1303
1893
  const hasParadigmPrecommit = beforeShellHooks.some(
1304
1894
  (h) => JSON.stringify(h).includes("paradigm-precommit.sh")
@@ -1316,6 +1906,10 @@ async function installCursorHooks(rootDir, force) {
1316
1906
  }
1317
1907
  async function hooksUninstallCommand(options = {}) {
1318
1908
  const rootDir = process.cwd();
1909
+ const dryRun = options.dryRun || false;
1910
+ if (dryRun) {
1911
+ console.log(chalk.cyan("\n [dry-run] Showing what would be removed:\n"));
1912
+ }
1319
1913
  if (!options.cursor) {
1320
1914
  const gitDir = path.join(rootDir, ".git");
1321
1915
  if (!fs.existsSync(gitDir)) {
@@ -1329,53 +1923,76 @@ async function hooksUninstallCommand(options = {}) {
1329
1923
  if (fs.existsSync(hookPath)) {
1330
1924
  const content = fs.readFileSync(hookPath, "utf8");
1331
1925
  if (content.includes("paradigm")) {
1332
- fs.unlinkSync(hookPath);
1926
+ if (dryRun) {
1927
+ console.log(chalk.gray(` Would remove: ${hookPath}`));
1928
+ } else {
1929
+ fs.unlinkSync(hookPath);
1930
+ }
1333
1931
  removed.push(hookName);
1334
1932
  }
1335
1933
  }
1336
1934
  }
1337
- if (removed.length > 0) {
1338
- console.log(chalk.green(`Git hooks removed: ${removed.join(", ")}`));
1339
- } else {
1340
- console.log(chalk.gray("No paradigm git hooks found to remove"));
1935
+ if (!dryRun) {
1936
+ if (removed.length > 0) {
1937
+ console.log(chalk.green(`Git hooks removed: ${removed.join(", ")}`));
1938
+ } else {
1939
+ console.log(chalk.gray("No paradigm git hooks found to remove"));
1940
+ }
1941
+ } else if (removed.length === 0) {
1942
+ console.log(chalk.gray(" No paradigm git hooks to remove"));
1341
1943
  }
1342
1944
  }
1343
1945
  if (options.cursor) {
1344
1946
  const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
1345
1947
  const cursorRemoved = [];
1346
- for (const hookName of ["paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh"]) {
1948
+ for (const hookName of ["paradigm-session-start.sh", "paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh", "paradigm-pretooluse.sh", "paradigm-posttooluse.sh"]) {
1347
1949
  const hookPath = path.join(cursorHooksDir, hookName);
1348
1950
  if (fs.existsSync(hookPath)) {
1349
- fs.unlinkSync(hookPath);
1951
+ if (dryRun) {
1952
+ console.log(chalk.gray(` Would remove: ${hookPath}`));
1953
+ } else {
1954
+ fs.unlinkSync(hookPath);
1955
+ }
1350
1956
  cursorRemoved.push(hookName);
1351
1957
  }
1352
1958
  }
1353
1959
  const hooksJsonPath = path.join(rootDir, ".cursor", "hooks.json");
1354
1960
  if (fs.existsSync(hooksJsonPath)) {
1355
- try {
1356
- const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
1357
- const hooks = hooksConfig.hooks || {};
1358
- for (const key of ["stop", "afterFileEdit", "beforeShellExecution"]) {
1359
- if (Array.isArray(hooks[key])) {
1360
- hooks[key] = hooks[key].filter(
1361
- (h) => !JSON.stringify(h).includes("paradigm-")
1362
- );
1363
- if (hooks[key].length === 0) {
1364
- delete hooks[key];
1961
+ if (dryRun) {
1962
+ console.log(chalk.gray(` Would clean paradigm entries from: ${hooksJsonPath}`));
1963
+ } else {
1964
+ try {
1965
+ const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
1966
+ const hooks = hooksConfig.hooks || {};
1967
+ for (const key of ["sessionStart", "stop", "afterFileEdit", "beforeShellExecution", "preToolUse", "postToolUse"]) {
1968
+ if (Array.isArray(hooks[key])) {
1969
+ hooks[key] = hooks[key].filter(
1970
+ (h) => !JSON.stringify(h).includes("paradigm-")
1971
+ );
1972
+ if (hooks[key].length === 0) {
1973
+ delete hooks[key];
1974
+ }
1365
1975
  }
1366
1976
  }
1977
+ hooksConfig.hooks = hooks;
1978
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1979
+ } catch {
1367
1980
  }
1368
- hooksConfig.hooks = hooks;
1369
- fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1370
- } catch {
1371
1981
  }
1372
1982
  }
1373
- if (cursorRemoved.length > 0) {
1374
- console.log(chalk.green(`Cursor hooks removed: ${cursorRemoved.join(", ")}`));
1375
- } else {
1376
- console.log(chalk.gray("No paradigm Cursor hooks found to remove"));
1983
+ if (!dryRun) {
1984
+ if (cursorRemoved.length > 0) {
1985
+ console.log(chalk.green(`Cursor hooks removed: ${cursorRemoved.join(", ")}`));
1986
+ } else {
1987
+ console.log(chalk.gray("No paradigm Cursor hooks found to remove"));
1988
+ }
1989
+ } else if (cursorRemoved.length === 0) {
1990
+ console.log(chalk.gray(" No paradigm Cursor hooks to remove"));
1377
1991
  }
1378
1992
  }
1993
+ if (dryRun) {
1994
+ console.log(chalk.cyan("\n [dry-run] No changes made.\n"));
1995
+ }
1379
1996
  }
1380
1997
  async function hooksStatusCommand() {
1381
1998
  const rootDir = process.cwd();
@@ -1471,7 +2088,7 @@ async function hooksStatusCommand() {
1471
2088
  }
1472
2089
  console.log(chalk.magenta("\n Cursor Hooks Status\n"));
1473
2090
  const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
1474
- const cursorHooks = ["paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh"];
2091
+ const cursorHooks = ["paradigm-session-start.sh", "paradigm-stop.sh", "paradigm-precommit.sh", "paradigm-postwrite.sh", "paradigm-pretooluse.sh", "paradigm-posttooluse.sh"];
1475
2092
  for (const hookName of cursorHooks) {
1476
2093
  const hookPath = path.join(cursorHooksDir, hookName);
1477
2094
  if (fs.existsSync(hookPath)) {
@@ -1485,11 +2102,17 @@ async function hooksStatusCommand() {
1485
2102
  try {
1486
2103
  const hooksJson = JSON.parse(fs.readFileSync(cursorHooksJsonPath, "utf8"));
1487
2104
  const hooks = hooksJson.hooks || {};
2105
+ const hasSessionStart = JSON.stringify(hooks.sessionStart || []).includes("paradigm-session-start.sh");
1488
2106
  const hasStop = JSON.stringify(hooks.stop || []).includes("paradigm-stop.sh");
1489
2107
  const hasPostwrite = JSON.stringify(hooks.afterFileEdit || []).includes("paradigm-postwrite.sh");
1490
2108
  const hasPrecommit = JSON.stringify(hooks.beforeShellExecution || []).includes("paradigm-precommit.sh");
2109
+ const hasPretoolUse = JSON.stringify(hooks.preToolUse || []).includes("paradigm-pretooluse.sh");
2110
+ const hasPosttoolUse = JSON.stringify(hooks.postToolUse || []).includes("paradigm-posttooluse.sh");
2111
+ console.log(chalk.gray(` hooks.json sessionStart: ${hasSessionStart ? "configured" : "missing"}`));
1491
2112
  console.log(chalk.gray(` hooks.json stop: ${hasStop ? "configured" : "missing"}`));
1492
2113
  console.log(chalk.gray(` hooks.json afterFileEdit: ${hasPostwrite ? "configured" : "missing"}`));
2114
+ console.log(chalk.gray(` hooks.json preToolUse: ${hasPretoolUse ? "configured" : "missing"}`));
2115
+ console.log(chalk.gray(` hooks.json postToolUse: ${hasPosttoolUse ? "configured" : "missing"}`));
1493
2116
  console.log(chalk.gray(` hooks.json beforeShellExecution: ${hasPrecommit ? "configured" : "missing"}`));
1494
2117
  } catch {
1495
2118
  console.log(chalk.yellow(" hooks.json: parse error"));