@a-company/paradigm 3.1.6 → 3.6.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-YCLN7WXV.js → chunk-2QNZ6PVD.js} +219 -35
  5. package/dist/{chunk-UM54F7G5.js → chunk-4N6AYEEA.js} +1 -1
  6. package/dist/{chunk-MVXJVRFI.js → chunk-5TUAVVIG.js} +65 -1
  7. package/dist/{chunk-5C4SGQKH.js → chunk-6P4IFIK2.js} +4 -2
  8. package/dist/{chunk-WS5KM7OL.js → chunk-6RNYVBSG.js} +1 -1
  9. package/dist/{chunk-VZ7CXFRZ.js → chunk-ADOBV4PH.js} +1387 -17
  10. package/dist/{chunk-N6PJAPDE.js → chunk-AK5M6KJB.js} +18 -0
  11. package/dist/{chunk-4LGLU2LO.js → chunk-GY5KO3YZ.js} +679 -183
  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-HXY6AY52.js → chunk-M2XMTJHQ.js} +667 -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-GPQ4LAY3.js → dist-OLFOTUHS.js} +26 -6
  30. package/dist/{dist-NHJQVVUW.js → dist-Q6SAZI7X.js} +2 -2
  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-ZJBAL4HD.js} +234 -5
  36. package/dist/{hooks-RLJFGKPF.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 +7982 -5065
  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-KDIGZWKT.js +63 -0
  50. package/dist/{server-MV4HNFVF.js → server-NN7WDAZJ.js} +4413 -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-YELZUPYG.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-B5W6GZLT.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
@@ -473,6 +474,107 @@ for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index
473
474
  done
474
475
 
475
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
+
476
578
  exit 0
477
579
  `;
478
580
  var CURSOR_STOP_HOOK = `#!/bin/sh
@@ -514,6 +616,26 @@ fi
514
616
 
515
617
  cd "$CWD" || exit 0
516
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
+
517
639
  # Get modified files (uncommitted changes)
518
640
  MODIFIED=$(git diff --name-only HEAD 2>/dev/null)
519
641
  if [ -z "$MODIFIED" ]; then
@@ -782,6 +904,13 @@ if [ "$VIOLATION_COUNT" -gt 0 ]; then
782
904
  echo " 4. paradigm_reindex \u2014 rebuild indexes after updates" >&2
783
905
  echo " 5. paradigm_lore_record \u2014 record session lore entry" >&2
784
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
+
785
914
  exit 2
786
915
  fi
787
916
 
@@ -792,19 +921,25 @@ if [ -n "$ADVISORY" ]; then
792
921
  echo "$ADVISORY" >&2
793
922
  fi
794
923
 
795
- # Clean up pending-review on pass
924
+ # Clean up pending-review and loop guard on pass
796
925
  rm -f ".paradigm/.pending-review"
797
926
  rm -f ".paradigm/.habits-blocking"
927
+ rm -f ".paradigm/.stop-hook-active"
798
928
 
799
929
  exit 0
800
930
  `;
801
931
  var CURSOR_POSTWRITE_HOOK = `#!/bin/sh
802
- # Paradigm Cursor PostWrite Hook (v2)
803
- # Fires after file edits.
804
- # Tracks modified source files in .paradigm/.pending-review
805
- # and outputs compliance reminders.
932
+ # Paradigm Cursor PostWrite Hook (v2) \u2014 LEGACY
933
+ # Fires after file edits via Cursor's afterFileEdit hook type.
806
934
  # Installed by: paradigm hooks install --cursor
807
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
+ #
808
943
  # Hook type: afterFileEdit
809
944
  # Exit 0 always (never blocks \u2014 advisory only)
810
945
 
@@ -882,18 +1017,8 @@ if [ -z "$found_purpose" ] && [ -f ".purpose" ]; then
882
1017
  found_purpose=".purpose"
883
1018
  fi
884
1019
 
885
- if [ -z "$found_purpose" ]; then
886
- file_dir=$(dirname "$REL_PATH")
887
- echo "" >&2
888
- echo "[paradigm] No .purpose file covers $file_dir/" >&2
889
- echo " Create one: paradigm_purpose_init + paradigm_purpose_add_component" >&2
890
- echo " $PENDING_COUNT file(s) pending review. The stop hook WILL BLOCK." >&2
891
- elif [ "$PENDING_COUNT" -gt 0 ] && [ "$((PENDING_COUNT % 3))" -eq 0 ]; then
892
- echo "" >&2
893
- echo "[paradigm] $PENDING_COUNT source file(s) modified. Update $found_purpose:" >&2
894
- echo " -> #components, ~aspects (with anchors), !signals, \\$flows, ^gates" >&2
895
- echo " The stop hook WILL BLOCK if .purpose files aren't updated." >&2
896
- fi
1020
+ # NOTE: No stderr output here \u2014 Cursor ignores afterFileEdit output.
1021
+ # Advisory messages are handled by cursor-posttooluse.sh (postToolUse hook).
897
1022
 
898
1023
  exit 0
899
1024
  `;
@@ -941,6 +1066,263 @@ for f in .paradigm/scan-index.json .paradigm/navigator.yaml .paradigm/flow-index
941
1066
  done
942
1067
 
943
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
+
944
1326
  exit 0
945
1327
  `;
946
1328
 
@@ -964,6 +1346,68 @@ function isParadigmPluginActive() {
964
1346
  return { active: false };
965
1347
  }
966
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
+ }
967
1411
  function cleanupProjectClaudeCodeHooks(rootDir) {
968
1412
  const removed = [];
969
1413
  const claudeHooksDir = path.join(rootDir, ".claude", "hooks");
@@ -1019,6 +1463,25 @@ function cleanupProjectClaudeCodeHooks(rootDir) {
1019
1463
  }
1020
1464
  return { cleaned: removed.length > 0, removed };
1021
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
+ }
1022
1485
  var POST_COMMIT_HOOK = `#!/bin/sh
1023
1486
  # Paradigm post-commit hook - captures history from commits
1024
1487
  # Installed by: paradigm hooks install
@@ -1115,8 +1578,41 @@ fi
1115
1578
  `;
1116
1579
  async function hooksInstallCommand(options = {}) {
1117
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
+ }
1118
1585
  const onlyClaudeCode = options.claudeCode && !options.postCommit && !options.prePush && !options.cursor;
1119
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
+ }
1120
1616
  if (!onlyClaudeCode && !onlyCursor) {
1121
1617
  const gitDir = path.join(rootDir, ".git");
1122
1618
  if (!fs.existsSync(gitDir)) {
@@ -1124,53 +1620,79 @@ async function hooksInstallCommand(options = {}) {
1124
1620
  return;
1125
1621
  }
1126
1622
  const hooksDir = path.join(gitDir, "hooks");
1127
- fs.mkdirSync(hooksDir, { recursive: true });
1128
- const installAll2 = !options.postCommit && !options.prePush && !options.claudeCode;
1623
+ const installAllGit = !options.postCommit && !options.prePush && !options.claudeCode;
1129
1624
  const installed = [];
1130
- if (installAll2 || options.postCommit) {
1625
+ if (installAllGit || options.postCommit) {
1131
1626
  const hookPath = path.join(hooksDir, "post-commit");
1132
- if (fs.existsSync(hookPath) && !options.force) {
1133
- const content = fs.readFileSync(hookPath, "utf8");
1134
- if (!content.includes("paradigm")) {
1135
- 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
+ }
1136
1638
  } else {
1137
- 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");
1138
1643
  }
1139
- } else {
1140
- fs.writeFileSync(hookPath, POST_COMMIT_HOOK);
1141
- fs.chmodSync(hookPath, "755");
1142
- installed.push("post-commit");
1143
1644
  }
1144
1645
  }
1145
- if (installAll2 || options.prePush) {
1646
+ if (installAllGit || options.prePush) {
1146
1647
  const hookPath = path.join(hooksDir, "pre-push");
1147
- if (fs.existsSync(hookPath) && !options.force) {
1148
- const content = fs.readFileSync(hookPath, "utf8");
1149
- if (!content.includes("paradigm")) {
1150
- 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
+ }
1151
1659
  } else {
1152
- 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");
1153
1664
  }
1154
- } else {
1155
- fs.writeFileSync(hookPath, PRE_PUSH_HOOK);
1156
- fs.chmodSync(hookPath, "755");
1157
- installed.push("pre-push");
1158
1665
  }
1159
1666
  }
1160
- if (installed.length > 0) {
1667
+ if (!dryRun && installed.length > 0) {
1161
1668
  console.log(chalk.green(`Git hooks installed: ${installed.join(", ")}`));
1162
1669
  }
1163
1670
  const historyDir = path.join(rootDir, ".paradigm/history");
1164
- if (!fs.existsSync(historyDir)) {
1671
+ if (!fs.existsSync(historyDir) && !dryRun) {
1165
1672
  console.log(chalk.gray("Tip: Run `paradigm history init` to initialize history tracking"));
1166
1673
  }
1167
1674
  }
1168
1675
  const installAll = !options.postCommit && !options.prePush && !options.claudeCode && !options.cursor;
1169
1676
  if (installAll || options.claudeCode) {
1170
- 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
+ }
1171
1684
  }
1172
1685
  if (installAll || options.cursor) {
1173
- 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"));
1174
1696
  }
1175
1697
  }
1176
1698
  async function installClaudeCodeHooks(rootDir, force) {
@@ -1272,9 +1794,12 @@ async function installCursorHooks(rootDir, force) {
1272
1794
  fs.mkdirSync(cursorHooksDir, { recursive: true });
1273
1795
  const installed = [];
1274
1796
  const hookScripts = [
1797
+ { name: "paradigm-session-start.sh", content: CURSOR_SESSION_START_HOOK },
1275
1798
  { name: "paradigm-stop.sh", content: CURSOR_STOP_HOOK },
1276
1799
  { name: "paradigm-precommit.sh", content: CURSOR_PRECOMMIT_HOOK },
1277
- { 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 }
1278
1803
  ];
1279
1804
  for (const hook of hookScripts) {
1280
1805
  const destPath = path.join(cursorHooksDir, hook.name);
@@ -1296,9 +1821,14 @@ async function installCursorHooks(rootDir, force) {
1296
1821
  }
1297
1822
  hooksConfig.version = 1;
1298
1823
  const hooks = hooksConfig.hooks || {};
1824
+ const paradigmSessionStartEntry = {
1825
+ command: ".cursor/hooks/paradigm-session-start.sh",
1826
+ timeout: 5
1827
+ };
1299
1828
  const paradigmStopEntry = {
1300
1829
  command: ".cursor/hooks/paradigm-stop.sh",
1301
- timeout: 10
1830
+ timeout: 10,
1831
+ loop_limit: 3
1302
1832
  };
1303
1833
  const paradigmPostwriteEntry = {
1304
1834
  command: ".cursor/hooks/paradigm-postwrite.sh",
@@ -1309,6 +1839,14 @@ async function installCursorHooks(rootDir, force) {
1309
1839
  matcher: "git commit",
1310
1840
  timeout: 30
1311
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;
1312
1850
  const stopHooks = hooks.stop || [];
1313
1851
  const hasParadigmStop = stopHooks.some(
1314
1852
  (h) => JSON.stringify(h).includes("paradigm-stop.sh")
@@ -1325,6 +1863,32 @@ async function installCursorHooks(rootDir, force) {
1325
1863
  afterFileEditHooks.push(paradigmPostwriteEntry);
1326
1864
  }
1327
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;
1328
1892
  const beforeShellHooks = hooks.beforeShellExecution || [];
1329
1893
  const hasParadigmPrecommit = beforeShellHooks.some(
1330
1894
  (h) => JSON.stringify(h).includes("paradigm-precommit.sh")
@@ -1342,6 +1906,10 @@ async function installCursorHooks(rootDir, force) {
1342
1906
  }
1343
1907
  async function hooksUninstallCommand(options = {}) {
1344
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
+ }
1345
1913
  if (!options.cursor) {
1346
1914
  const gitDir = path.join(rootDir, ".git");
1347
1915
  if (!fs.existsSync(gitDir)) {
@@ -1355,53 +1923,76 @@ async function hooksUninstallCommand(options = {}) {
1355
1923
  if (fs.existsSync(hookPath)) {
1356
1924
  const content = fs.readFileSync(hookPath, "utf8");
1357
1925
  if (content.includes("paradigm")) {
1358
- fs.unlinkSync(hookPath);
1926
+ if (dryRun) {
1927
+ console.log(chalk.gray(` Would remove: ${hookPath}`));
1928
+ } else {
1929
+ fs.unlinkSync(hookPath);
1930
+ }
1359
1931
  removed.push(hookName);
1360
1932
  }
1361
1933
  }
1362
1934
  }
1363
- if (removed.length > 0) {
1364
- console.log(chalk.green(`Git hooks removed: ${removed.join(", ")}`));
1365
- } else {
1366
- 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"));
1367
1943
  }
1368
1944
  }
1369
1945
  if (options.cursor) {
1370
1946
  const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
1371
1947
  const cursorRemoved = [];
1372
- 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"]) {
1373
1949
  const hookPath = path.join(cursorHooksDir, hookName);
1374
1950
  if (fs.existsSync(hookPath)) {
1375
- fs.unlinkSync(hookPath);
1951
+ if (dryRun) {
1952
+ console.log(chalk.gray(` Would remove: ${hookPath}`));
1953
+ } else {
1954
+ fs.unlinkSync(hookPath);
1955
+ }
1376
1956
  cursorRemoved.push(hookName);
1377
1957
  }
1378
1958
  }
1379
1959
  const hooksJsonPath = path.join(rootDir, ".cursor", "hooks.json");
1380
1960
  if (fs.existsSync(hooksJsonPath)) {
1381
- try {
1382
- const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
1383
- const hooks = hooksConfig.hooks || {};
1384
- for (const key of ["stop", "afterFileEdit", "beforeShellExecution"]) {
1385
- if (Array.isArray(hooks[key])) {
1386
- hooks[key] = hooks[key].filter(
1387
- (h) => !JSON.stringify(h).includes("paradigm-")
1388
- );
1389
- if (hooks[key].length === 0) {
1390
- 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
+ }
1391
1975
  }
1392
1976
  }
1977
+ hooksConfig.hooks = hooks;
1978
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1979
+ } catch {
1393
1980
  }
1394
- hooksConfig.hooks = hooks;
1395
- fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksConfig, null, 2) + "\n", "utf8");
1396
- } catch {
1397
1981
  }
1398
1982
  }
1399
- if (cursorRemoved.length > 0) {
1400
- console.log(chalk.green(`Cursor hooks removed: ${cursorRemoved.join(", ")}`));
1401
- } else {
1402
- 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"));
1403
1991
  }
1404
1992
  }
1993
+ if (dryRun) {
1994
+ console.log(chalk.cyan("\n [dry-run] No changes made.\n"));
1995
+ }
1405
1996
  }
1406
1997
  async function hooksStatusCommand() {
1407
1998
  const rootDir = process.cwd();
@@ -1497,7 +2088,7 @@ async function hooksStatusCommand() {
1497
2088
  }
1498
2089
  console.log(chalk.magenta("\n Cursor Hooks Status\n"));
1499
2090
  const cursorHooksDir = path.join(rootDir, ".cursor", "hooks");
1500
- 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"];
1501
2092
  for (const hookName of cursorHooks) {
1502
2093
  const hookPath = path.join(cursorHooksDir, hookName);
1503
2094
  if (fs.existsSync(hookPath)) {
@@ -1511,11 +2102,17 @@ async function hooksStatusCommand() {
1511
2102
  try {
1512
2103
  const hooksJson = JSON.parse(fs.readFileSync(cursorHooksJsonPath, "utf8"));
1513
2104
  const hooks = hooksJson.hooks || {};
2105
+ const hasSessionStart = JSON.stringify(hooks.sessionStart || []).includes("paradigm-session-start.sh");
1514
2106
  const hasStop = JSON.stringify(hooks.stop || []).includes("paradigm-stop.sh");
1515
2107
  const hasPostwrite = JSON.stringify(hooks.afterFileEdit || []).includes("paradigm-postwrite.sh");
1516
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"}`));
1517
2112
  console.log(chalk.gray(` hooks.json stop: ${hasStop ? "configured" : "missing"}`));
1518
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"}`));
1519
2116
  console.log(chalk.gray(` hooks.json beforeShellExecution: ${hasPrecommit ? "configured" : "missing"}`));
1520
2117
  } catch {
1521
2118
  console.log(chalk.yellow(" hooks.json: parse error"));