@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.
- package/dist/{accept-orchestration-CWZNCGZX.js → accept-orchestration-DIGPJVUR.js} +6 -5
- package/dist/{aggregate-W7Q6VIM2.js → aggregate-V4KPR3RW.js} +2 -2
- package/dist/{beacon-B47XSTL7.js → beacon-XRXL5KZB.js} +2 -2
- package/dist/{chunk-YCLN7WXV.js → chunk-2QNZ6PVD.js} +219 -35
- package/dist/{chunk-UM54F7G5.js → chunk-4N6AYEEA.js} +1 -1
- package/dist/{chunk-MVXJVRFI.js → chunk-5TUAVVIG.js} +65 -1
- package/dist/{chunk-5C4SGQKH.js → chunk-6P4IFIK2.js} +4 -2
- package/dist/{chunk-WS5KM7OL.js → chunk-6RNYVBSG.js} +1 -1
- package/dist/{chunk-VZ7CXFRZ.js → chunk-ADOBV4PH.js} +1387 -17
- package/dist/{chunk-N6PJAPDE.js → chunk-AK5M6KJB.js} +18 -0
- package/dist/{chunk-4LGLU2LO.js → chunk-GY5KO3YZ.js} +679 -183
- package/dist/{chunk-MC7XC7XQ.js → chunk-GZDFVP2N.js} +20 -13
- package/dist/chunk-HPC3JAUP.js +42 -0
- package/dist/chunk-IRVA7NKV.js +657 -0
- package/dist/{chunk-ZPN7MXRA.js → chunk-KFHK6EBI.js} +184 -1
- package/dist/{chunk-UUZ2DMG5.js → chunk-KWDTBXP2.js} +1 -1
- package/dist/{chunk-HXY6AY52.js → chunk-M2XMTJHQ.js} +667 -70
- package/dist/{chunk-PW2EXJQT.js → chunk-MRENOFTR.js} +24 -1
- package/dist/{chunk-QS36NGWV.js → chunk-QHJGB5TV.js} +1 -1
- package/dist/chunk-UI3XXVJ6.js +449 -0
- package/dist/{chunk-AD2LSCHB.js → chunk-Y4XZWCHK.js} +40 -74
- package/dist/{constellation-K3CIQCHI.js → constellation-GNK5DIMH.js} +2 -2
- package/dist/{cost-AEK6R7HK.js → cost-AGO5N7DD.js} +1 -1
- package/dist/{cursorrules-KI5QWHIX.js → cursorrules-LQFA7M62.js} +2 -2
- package/dist/{delete-W67IVTLJ.js → delete-3YXAJ5AA.js} +12 -1
- package/dist/{diff-AJJ5H6HV.js → diff-J6C5IHPV.js} +6 -5
- package/dist/{dist-2F7NO4H4-KSL6SJIO.js → dist-AG5JNIZU-XSEZ2LLK.js} +28 -3
- package/dist/dist-JOHRYQUA.js +7294 -0
- package/dist/{dist-GPQ4LAY3.js → dist-OLFOTUHS.js} +26 -6
- package/dist/{dist-NHJQVVUW.js → dist-Q6SAZI7X.js} +2 -2
- package/dist/{doctor-JBIV5PMN.js → doctor-TQYRF7KK.js} +2 -2
- package/dist/{edit-Y7XPYSMK.js → edit-EOMPXOG5.js} +1 -1
- package/dist/flow-7JUH6D4H.js +185 -0
- package/dist/global-AXILUM5X.js +136 -0
- package/dist/{habits-FA65W77Y.js → habits-ZJBAL4HD.js} +234 -5
- package/dist/{hooks-RLJFGKPF.js → hooks-DLZEYHI3.js} +1 -1
- package/dist/index.js +125 -100
- package/dist/{lint-HXKTWRNO.js → lint-N4LMMEXH.js} +141 -1
- package/dist/{list-R3QWW4SC.js → list-JKBJ7ESH.js} +1 -1
- package/dist/mcp.js +7982 -5065
- package/dist/{orchestrate-4ZH5GUQH.js → orchestrate-FAV64G2R.js} +6 -5
- package/dist/{probe-OYCP4JYG.js → probe-X3J2JX62.js} +18 -3
- package/dist/{promote-E6NBZ3BK.js → promote-HZH5E5CO.js} +1 -1
- package/dist/{providers-4PGPZEWP.js → providers-NQ67LO2Z.js} +1 -1
- package/dist/{record-OHQNWOUP.js → record-EECZ3E4I.js} +1 -1
- package/dist/{remember-6VZ74B7E.js → remember-3KJZGDUG.js} +1 -1
- package/dist/{review-RUHX25A5.js → review-BF26ILZB.js} +1 -1
- package/dist/{ripple-SBQOSTZD.js → ripple-JIUAMBLA.js} +2 -2
- package/dist/sentinel-KDIGZWKT.js +63 -0
- package/dist/{server-MV4HNFVF.js → server-NN7WDAZJ.js} +4413 -9
- package/dist/{setup-DF4F3ICN.js → setup-363IB6MO.js} +1 -1
- package/dist/{setup-JHBPZAG7.js → setup-UKJ3VGHI.js} +4 -4
- package/dist/{shift-YELZUPYG.js → shift-KDVYB6CR.js} +16 -13
- package/dist/{show-WTOJXUTN.js → show-SAMTXEHG.js} +1 -1
- package/dist/{snapshot-GTVPRYZG.js → snapshot-KCMONZAO.js} +2 -2
- package/dist/{spawn-BJRQA2NR.js → spawn-EO7B2UM3.js} +2 -2
- package/dist/{summary-5SBFO7QK.js → summary-E2PU4UN2.js} +3 -3
- package/dist/{switch-6EANJ7O6.js → switch-CC2KACXO.js} +1 -1
- package/dist/{sync-5KSTPJ4B.js → sync-5VJPZQNX.js} +2 -2
- package/dist/sync-llms-7QDA3ZWC.js +166 -0
- package/dist/{team-NWP2KJAB.js → team-6CCNANKE.js} +7 -6
- package/dist/{test-MA5TWJQV.js → test-DK2RWLTK.js} +91 -8
- package/dist/{thread-JCJVRUQR.js → thread-RNSLADXN.js} +18 -2
- package/dist/{timeline-P7BARFLI.js → timeline-TJDVVVA3.js} +1 -1
- package/dist/{triage-TBIWJA6R.js → triage-B5W6GZLT.js} +2 -2
- package/dist/university-content/courses/para-101.json +2 -1
- package/dist/university-content/courses/para-201.json +102 -3
- package/dist/university-content/courses/para-301.json +14 -11
- package/dist/university-content/courses/para-401.json +57 -3
- package/dist/university-content/courses/para-501.json +204 -6
- package/dist/university-content/plsat/v3.0.json +808 -3
- package/dist/university-content/reference.json +270 -0
- package/dist/{upgrade-TIYFQYPO.js → upgrade-RBSE4M6I.js} +1 -1
- package/dist/{validate-QEEY6KFS.js → validate-2LTHHORX.js} +1 -1
- package/dist/{watch-4LT4O6K7.js → watch-NBPOMOEX.js} +76 -0
- package/dist/{watch-2XEYUH43.js → watch-PAEH6MOG.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-GWM2WRXL.js +0 -1095
- package/dist/sentinel-WB7GIK4V.js +0 -43
- /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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
1128
|
-
const installAll2 = !options.postCommit && !options.prePush && !options.claudeCode;
|
|
1623
|
+
const installAllGit = !options.postCommit && !options.prePush && !options.claudeCode;
|
|
1129
1624
|
const installed = [];
|
|
1130
|
-
if (
|
|
1625
|
+
if (installAllGit || options.postCommit) {
|
|
1131
1626
|
const hookPath = path.join(hooksDir, "post-commit");
|
|
1132
|
-
if (
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
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 (
|
|
1646
|
+
if (installAllGit || options.prePush) {
|
|
1146
1647
|
const hookPath = path.join(hooksDir, "pre-push");
|
|
1147
|
-
if (
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
)
|
|
1389
|
-
|
|
1390
|
-
|
|
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 (
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
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"));
|