@boyingliu01/xp-gate 0.10.17 → 0.11.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/hooks/pre-commit CHANGED
@@ -11,16 +11,17 @@
11
11
  # 1. Global: ~/.config/xp-gate/adapters (for core.hooksPath global setup)
12
12
  # 2. Project-local: <repo>/githooks/ (for per-project init)
13
13
  # 3. Script dir: hooks directory containing this script (fallback)
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
15
  ADAPTER_DIR=""
15
16
  GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"
16
17
  PROJECT_GITHOOKS="$(git rev-parse --show-toplevel 2>/dev/null)/githooks"
18
+ GATES_DIR="$SCRIPT_DIR/gates"
17
19
 
18
20
  if [ -f "$GLOBAL_ADAPTER_DIR/adapter-common.sh" ]; then
19
21
  ADAPTER_DIR="$GLOBAL_ADAPTER_DIR"
20
22
  elif [ -f "$PROJECT_GITHOOKS/adapter-common.sh" ]; then
21
23
  ADAPTER_DIR="$PROJECT_GITHOOKS"
22
24
  else
23
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
25
  ADAPTER_DIR="$SCRIPT_DIR"
25
26
  fi
26
27
  source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
@@ -29,6 +30,49 @@ source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
29
30
  exit 1
30
31
  }
31
32
 
33
+ # Resolve adapter file path — handles both layouts:
34
+ # Global: ~/.config/xp-gate/adapters/typescript.sh (flat)
35
+ # Project: <repo>/githooks/adapters/typescript.sh (nested)
36
+ # Script dir: <hook-dir>/adapters/typescript.sh (nested)
37
+ # ADAPTER_DIR points to the base directory that contains either
38
+ # adapter-common.sh directly (global) or adapter-common.sh in a flat layout.
39
+ # Gate scripts live alongside adapter-common.sh.
40
+ resolve_adapter_path() {
41
+ local lang="$1"
42
+ # Try flat layout first (global: typescript.sh next to adapter-common.sh)
43
+ if [ -f "$ADAPTER_DIR/${lang}.sh" ]; then
44
+ echo "$ADAPTER_DIR/${lang}.sh"
45
+ return 0
46
+ fi
47
+ # Try nested layout (project: adapters/typescript.sh)
48
+ if [ -f "$ADAPTER_DIR/adapters/${lang}.sh" ]; then
49
+ echo "$ADAPTER_DIR/adapters/${lang}.sh"
50
+ return 0
51
+ fi
52
+ # Try project githooks as fallback
53
+ if [ -f "$PROJECT_GITHOOKS/adapters/${lang}.sh" ]; then
54
+ echo "$PROJECT_GITHOOKS/adapters/${lang}.sh"
55
+ return 0
56
+ fi
57
+ # Try script dir as last resort
58
+ if [ -f "$SCRIPT_DIR/adapters/${lang}.sh" ]; then
59
+ echo "$SCRIPT_DIR/adapters/${lang}.sh"
60
+ return 0
61
+ fi
62
+ return 1
63
+ }
64
+
65
+ # Gate scripts directory — falls back to project githooks if ADAPTER_DIR doesn't have them
66
+ if [ -f "$ADAPTER_DIR/gate-3.sh" ]; then
67
+ GATE_DIR="$ADAPTER_DIR"
68
+ elif [ -f "$PROJECT_GITHOOKS/gate-3.sh" ]; then
69
+ GATE_DIR="$PROJECT_GITHOOKS"
70
+ elif [ -f "$SCRIPT_DIR/gate-3.sh" ]; then
71
+ GATE_DIR="$SCRIPT_DIR"
72
+ else
73
+ GATE_DIR="$ADAPTER_DIR"
74
+ fi
75
+
32
76
  # Trap: ensure quality report generated on ANY exit (pass or fail).
33
77
  # Using a guard prevents recursion when 'exit' is called from inside the trap.
34
78
  # IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
@@ -132,7 +176,7 @@ echo ""
132
176
  # ============================================================================
133
177
 
134
178
  CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
135
- PROTECTED_BRANCHES="main master develop trunk mainline"
179
+ PROTECTED_BRANCHES="master develop trunk mainline"
136
180
  ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
137
181
 
138
182
  # Optional: project-level .protected-branches config file
@@ -155,7 +199,7 @@ if [ "$is_protected" = "true" ]; then
155
199
  # Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
156
200
  # Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
157
201
  if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
158
- echo " PASSED - Gate 0: Version Consistency Check (SKIP_VERSION_CHECK=1 env var)"
202
+ echo "⏭️ SKIPPED - Gate 0: Version consistency (SKIP_VERSION_CHECK=1 env var)"
159
203
  else
160
204
  # Check if staged changes include VERSION or CHANGELOG.md
161
205
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
@@ -540,13 +584,13 @@ GATE_1_START=$(gate_start_ms)
540
584
 
541
585
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
542
586
  # Documentation-only project - skip static analysis
543
- echo " PASSED - Skipped (no source code to analyze)."
544
-
587
+ echo "⏭️ SKIPPED - Code quality (documentation project)."
588
+
545
589
  elif [ "$PROJECT_LANG" = "typescript" ]; then
546
590
  # TypeScript: static analysis (tsc) + linting (eslint)
547
591
  if ! command -v npx &> /dev/null; then
548
- echo "ℹ️ SKIP - npx not available (non-blocking for TypeScript)"
549
- echo " PASSED - Code Quality Gate (SKIP)"
592
+ echo "ℹ️ npx not available skipping TypeScript checks"
593
+ echo "⏭️ SKIPPED - Code quality (npx not available)"
550
594
  else
551
595
  # Verify tsc is real TypeScript compiler (npx may fetch deprecated tsc@2.0.4 placeholder)
552
596
  TSC_OUTPUT=$(npx tsc --version 2>&1)
@@ -625,14 +669,36 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
625
669
  fi
626
670
  else
627
671
  echo "ℹ️ No ESLint configuration found - Skipping"
672
+ if [ -f "package.json" ] && command -v node &> /dev/null; then
673
+ HAS_ESLINT=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d.eslint||deps.eslint)?'yes':'no')}catch{console.log('no')}") && true
674
+ if [ "$HAS_ESLINT" = "yes" ]; then
675
+ echo "⚠️ WARNING: eslint is installed in package.json but no eslint config found"
676
+ echo " Either create .eslintrc.* / eslint.config.* or remove eslint from dependencies"
677
+ echo " See: https://eslint.org/docs/latest/use/configure/configuration-files"
678
+ fi
679
+ fi
680
+ fi
681
+
682
+ # ── Biome config detection ──
683
+ if ! [ -f "biome.json" ] && ! [ -f "biome.jsonc" ]; then
684
+ if [ -f "package.json" ] && command -v node &> /dev/null; then
685
+ HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
686
+ if [ "$HAS_BIOME" = "yes" ]; then
687
+ echo ""
688
+ echo "⚠️ WARNING: @biomejs/biome is installed in package.json but no biome.json found"
689
+ echo " Create a biome.json configuration file to enable Biome linting."
690
+ echo " NOTE: useLiteralKeys conflicts with TypeScript TS4111 — see docs/biome-configuration.md"
691
+ echo " For quick setup: add '\"linter\":{\"rules\":{\"complexity\":{\"useLiteralKeys\":\"off\"}}}' to biome.json"
692
+ fi
693
+ fi
628
694
  fi
629
695
  fi
630
696
 
631
697
  elif [ "$PROJECT_LANG" = "python" ]; then
632
698
  # Python: Ruff (includes syntax + linting) + mypy (optional typing)
633
699
  if ! command -v ruff &> /dev/null; then
634
- echo "ℹ️ SKIP - ruff not available (non-blocking for Python)"
635
- echo " PASSED - Code Quality Gate (SKIP)"
700
+ echo "ℹ️ ruff not available skipping Python linting"
701
+ echo "⏭️ SKIPPED - Code quality (ruff not available, run: pip install ruff)"
636
702
  else
637
703
  # Run Ruff (includes syntax check, linting)
638
704
  echo "Running Ruff linting (syntax + lint)..."
@@ -711,8 +777,8 @@ elif [ "$PROJECT_LANG" = "python" ]; then
711
777
  elif [ "$PROJECT_LANG" = "go" ]; then
712
778
  # Go: go vet + golangci-lint
713
779
  if ! command -v golangci-lint &> /dev/null; then
714
- echo "ℹ️ SKIP - golangci-lint not available (non-blocking for Go)"
715
- echo " PASSED - Code Quality Gate (SKIP)"
780
+ echo "ℹ️ golangci-lint not available skipping Go linting"
781
+ echo "⏭️ SKIPPED - Code quality (golangci-lint not available)"
716
782
  else
717
783
  echo "Running golangci-lint (comprehensive static analysis)..."
718
784
  golangci-lint run 2>&1 | head -30
@@ -799,7 +865,8 @@ elif [ "$PROJECT_LANG" = "shell" ] || echo "$CHANGED_FILES" | grep -qE '\.sh$';
799
865
 
800
866
  else
801
867
  # Route to appropriate adapter for other languages
802
- if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
868
+ ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
869
+ if [ -n "$ADAPTER_FILE" ] && source "$ADAPTER_FILE" 2>/dev/null; then
803
870
  echo "Running static analysis for $PROJECT_LANG..."
804
871
  run_static_analysis | head -30 2>/dev/null
805
872
  ANALYSIS_EXIT=$?
@@ -817,13 +884,13 @@ else
817
884
  if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
818
885
  echo "✅ PASSED - Language-specific code quality checks."
819
886
  else
820
- # If tools are not available or failed, skip non-blockingly
821
- echo " PASSED - Code Quality Gate (SKIP for unavailable tools)"
887
+ # Tools unavailable or failed skip non-blockingly
888
+ echo "⏭️ SKIPPED - Code quality (tools unavailable or failed)"
822
889
  fi
823
890
  else
824
891
  # If no adapter exists for this language, skip
825
- echo "ℹ️ No specific adapter for $PROJECT_LANG - using generic checks if any"
826
- echo " PASSED - Code Quality Gate (no specific checks available)."
892
+ echo "ℹ️ No specific adapter for $PROJECT_LANG using generic checks if any"
893
+ echo "⏭️ SKIPPED - Code quality (no specific checks for $PROJECT_LANG)"
827
894
  fi
828
895
  fi
829
896
  GATE_1_STATUS="PASS"
@@ -839,8 +906,8 @@ record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"
839
906
  GATE_2_START=$(gate_start_ms)
840
907
 
841
908
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
842
- echo " PASSED - Skipped (no source code to analyze)."
843
-
909
+ echo "⏭️ SKIPPED - Duplicate code (documentation project)."
910
+
844
911
  elif [ "$PROJECT_LANG" = "typescript" ]; then
845
912
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
846
913
  exit 1
@@ -886,11 +953,11 @@ elif [ "$PROJECT_LANG" = "go" ]; then
886
953
 
887
954
  elif [ "$PROJECT_LANG" = "powershell" ]; then
888
955
  echo "ℹ️ No PowerShell-native duplicate detector (jscpd does not support .ps1)"
889
- echo " PASSED - Skipped (no standardized tool for PowerShell duplicate detection)"
956
+ echo "⏭️ SKIPPED - Duplicate code (no tool for PowerShell)"
890
957
 
891
958
  elif [ "$PROJECT_LANG" = "shell" ]; then
892
959
  echo "ℹ️ Duplicate detection not typically used for shell scripts"
893
- echo " PASSED - Skipped (shell scripts, no standardized dup tool)"
960
+ echo "⏭️ SKIPPED - Duplicate code (no tool for shell scripts)"
894
961
 
895
962
  else
896
963
  SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
@@ -909,132 +976,16 @@ GATE_2_STATUS="PASS"
909
976
  record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"
910
977
 
911
978
  # ============================================================================
912
- # GATE 3: Cyclomatic Complexity Check (Old Gate 7)
913
- # Uses lizard - keeps existing logic for consistency
979
+ # GATE 3: Cyclomatic Complexity Check
980
+ # Extracted to gate-3.sh for maintainability
914
981
  # ============================================================================
915
- 2>&1 echo ""
916
- 2>&1 echo "→ Gate 3: Cyclomatic complexity..."
917
- GATE_3_START=$(gate_start_ms)
918
-
919
- if [ "$PROJECT_LANG" = "documentation-only" ]; then
920
- echo "✅ PASSED - Skipped (documentation project)."
921
-
922
- elif [ "$PROJECT_LANG" = "powershell" ]; then
923
- echo "ℹ️ No PowerShell principles checker available"
924
- echo "✅ PASSED - Skipped (no PowerShell Clean Code / SOLID tool)"
925
-
926
- else
927
- CCN_THRESHOLD=5
928
-
929
- # Check lizard availability
930
- LIZARD_CMD=""
931
- if command -v lizard > /dev/null 2>&1; then
932
- LIZARD_CMD=lizard
933
- elif [ -f ~/.local/bin/lizard ]; then
934
- LIZARD_CMD=~/.local/bin/lizard
935
- fi
936
-
937
- if [ -n "$LIZARD_CMD" ]; then
938
- LIZARD_PATH=$(eval echo "$LIZARD_CMD")
939
-
940
- # Get changed source files for complexity check
941
- CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
942
-
943
- if [ -n "$CC_FILES" ]; then
944
- echo "Checking complexity for source files..."
945
-
946
- # Run lizard with CCN threshold
947
- CC_OUTPUT=$($LIZARD_PATH -C $CCN_THRESHOLD $CC_FILES 2>&1 || true)
948
-
949
- # Parse warning count from the summary table: "Warning cnt 8"
950
- # Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
951
- CC_WARNINGS=$(echo "$CC_OUTPUT" | grep "^Warning cnt" | awk '{print $NF}' | tr -d '[:space:]' | sed 's/[^0-9]//g' || true)
952
- CC_WARNINGS=${CC_WARNINGS:-0}
953
-
954
- if [ "$CC_WARNINGS" -gt 0 ]; then
955
- echo "$CC_OUTPUT"
956
- echo ""
957
- echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
958
- echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
959
- exit 1
960
- else
961
- echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
962
- fi
963
- else
964
- echo "✅ PASSED - No source files to check for complexity."
965
- fi
966
- else
967
- echo "⚠️ WARN - lizard not installed, complexity check not performed"
968
- echo " Install with: pip install --user lizard"
969
- echo " Gate 3: Complexity check (WARN, tool not available)"
970
- GATE_3_STATUS="WARN"
971
- fi
972
- fi
973
- GATE_3_STATUS="PASS"
974
- record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
982
+ source "$GATE_DIR/gate-3.sh"
975
983
 
976
984
  # ============================================================================
977
- # GATE 4: Principles Checker (Old Gate 6 - Clean Code + SOLID)
978
- # Reuses existing principles checker logic
985
+ # GATE 4: Principles Checker (Clean Code + SOLID)
986
+ # Extracted to gate-4.sh for maintainability
979
987
  # ============================================================================
980
- 2>&1 echo ""
981
- 2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
982
- GATE_4_START=$(gate_start_ms)
983
-
984
- if [ "$PROJECT_LANG" = "documentation-only" ]; then
985
- echo "✅ PASSED - Skipped (documentation project)."
986
-
987
- else
988
- # Get source files to check against principles
989
- PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
990
-
991
- if [ -n "$PRINCIPLES_FILES" ]; then
992
- # Check if principles checker exists in project
993
- if [ -f "src/principles/index.ts" ]; then
994
- echo "Checking Clean Code + SOLID principles..."
995
-
996
- if command -v npx > /dev/null 2>&1; then
997
- # Run principles checker and store results
998
- if npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format json > /tmp/principles-output.json 2>/dev/null; then
999
- # Check severity levels
1000
- ERROR_COUNT=$(grep -c '"severity":"error"' /tmp/principles-output.json 2>/dev/null || true)
1001
- ERROR_COUNT=${ERROR_COUNT:-0}
1002
- WARNING_COUNT=$(grep -c '"severity":"warning"' /tmp/principles-output.json 2>/dev/null || true)
1003
- WARNING_COUNT=${WARNING_COUNT:-0}
1004
-
1005
- if [ "$ERROR_COUNT" -gt 0 ]; then
1006
- echo ""
1007
- echo "❌ BLOCKED - $ERROR_COUNT principle ERROR(S) found"
1008
- echo "Critical violations must be fixed before commit:"
1009
- echo " - error-handling violations"
1010
- echo " - SOLID principle violations"
1011
- echo " - architectural violations"
1012
- npx tsx src/principles/index.ts --files $PRINCIPLES_FILES --format console
1013
- exit 1
1014
- fi
1015
-
1016
- echo "✅ PASSED - Principles checker (no errors found)."
1017
- if [ "$WARNING_COUNT" -gt 0 ]; then
1018
- echo "ℹ️ $WARNING_COUNT warnings found (will be handled by Boy Scout Rule)."
1019
- fi
1020
- else
1021
- echo "⚠️ Warning: Principles checker execution failed"
1022
- echo "✅ PASSED - Principles check (SKIP, execution issue)"
1023
- fi
1024
- else
1025
- echo "ℹ️ npx not available - skipping principles check"
1026
- echo "✅ PASSED - Principles check (SKIP, no Node.js)"
1027
- fi
1028
- else
1029
- echo "ℹ️ Principles checker not found in project - skipping"
1030
- echo "✅ PASSED - Principles check (SKIP, not available in project)"
1031
- fi
1032
- else
1033
- echo "✅ PASSED - No source files changed (principles check skipped)."
1034
- fi
1035
- fi
1036
- GATE_4_STATUS="PASS"
1037
- record_gate_audit "gate-4" "principles" "$GATE_4_STATUS" "${WARNING_COUNT:-0}" "$GATE_4_START"
988
+ source "$GATE_DIR/gate-4.sh"
1038
989
 
1039
990
  # ============================================================================
1040
991
  # GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
@@ -1049,15 +1000,133 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
1049
1000
 
1050
1001
  else
1051
1002
  # ========================================================================
1052
- # Gate 5a: Test-Source File Pairing Check (WARNING only)
1003
+ # Gate 5a: Test-Source File Pairing Check
1004
+ # New .ts/.tsx files: BLOCK (no corresponding test → commit rejected)
1005
+ # Modified files (any lang): WARNING (backward compatible)
1006
+ # New non-TS files: WARNING (unchanged)
1007
+ # Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only, audit logged)
1008
+ # Grace period: .tdd-adoption.yaml gracePeriod: N (first N commits = WARNING only)
1053
1009
  # Checks coexistence, not temporal order. True "test-first" enforced by
1054
1010
  # Agent skills (Layer 1 + Layer 4).
1055
1011
  # ========================================================================
1056
- NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' || true)
1057
1012
 
1058
- if [ -n "$NEW_SOURCE_FILES" ]; then
1013
+ # --- Pre-checks: grace period + escape valve ---
1014
+ TDD_GRACE=0
1015
+ if [ -f ".tdd-adoption.yaml" ]; then
1016
+ TDD_ENABLED=$(grep -E '^\s*enabled:' .tdd-adoption.yaml | awk '{print $2}' || echo "true")
1017
+ if [ "$TDD_ENABLED" != "false" ]; then
1018
+ TDD_GRACE=$(grep -E '^\s*gracePeriod:' .tdd-adoption.yaml | awk '{print $2}' || echo "0")
1019
+ fi
1020
+ fi
1021
+
1022
+ TDD_BLOCK_DOWNGRADE=false
1023
+ if [ "$TDD_GRACE" -gt 0 ] 2>/dev/null; then
1024
+ TDD_BLOCK_DOWNGRADE=true
1025
+ fi
1026
+
1027
+ SKIP_5A_BLOCK=false
1028
+ if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
1029
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
1030
+ if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
1031
+ REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
1032
+ mkdir -p .xp-gate/reports
1033
+ # Escape special characters in REASON for valid JSON
1034
+ ESCAPED_REASON=$(printf '%s' "$REASON" | sed 's/\\/\\\\/g; s/"/\\"/g')
1035
+ ESCAPED_USER=$(printf '%s' "$(git config user.name)" | sed 's/\\/\\\\/g; s/"/\\"/g')
1036
+ echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"branch\":\"$CURRENT_BRANCH\",\"user\":\"$ESCAPED_USER\",\"reason\":\"$ESCAPED_REASON\",\"gate\":\"5a-block\"}" >> .xp-gate/reports/escape-valve-log.json
1037
+ echo "⚠️ ESCAPE VALVE: SKIP_GATE_5A_BLOCK=1 on branch $CURRENT_BRANCH (reason: $REASON)"
1038
+ echo " ⚠️ Post-mortem issue required within 24h"
1039
+ SKIP_5A_BLOCK=true
1040
+ else
1041
+ echo "❌ ESCAPE VALVE BLOCKED: SKIP_GATE_5A_BLOCK not allowed on main/master branch"
1042
+ exit 1
1043
+ fi
1044
+ fi
1045
+
1046
+ # --- BLOCK path: New .ts/.tsx files without corresponding tests ---
1047
+ NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v 'node_modules/' | grep -v '\.next/' | grep -v '\.nuxt/' | grep -v 'dist/' | grep -v 'build/' | grep -v '.turbo/' | grep -v '.cache/' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)
1048
+
1049
+ if [ -n "$NEW_TS_FILES" ]; then
1050
+ BLOCKED_TS_FILES=0
1051
+ for src_file in $NEW_TS_FILES; do
1052
+ base="${src_file%.*}"
1053
+ ext="${src_file##*.}"
1054
+ filename="${base##*/}"
1055
+ dir="$(dirname "$src_file")"
1056
+
1057
+ # Skip excluded files: index/barrel, types, interfaces, constants, declarations
1058
+ case "$filename" in
1059
+ index|types|interfaces|constants|__init__) continue ;;
1060
+ esac
1061
+ case "$ext" in
1062
+ d.ts|pyi) continue ;;
1063
+ esac
1064
+ # Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
1065
+ # Order matters: @no-test-required must be checked first — @no-test is a substring match
1066
+ if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
1067
+ if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
1068
+ continue
1069
+ fi
1070
+ # @no-test-required with short reason (< 10 chars) — fall through to BLOCK
1071
+ elif grep -q '@no-test' "$src_file" 2>/dev/null; then
1072
+ continue
1073
+ fi
1074
+
1075
+ # Check common test file patterns for TypeScript
1076
+ TEST_FOUND=false
1077
+ patterns=(
1078
+ "${base}.test.${ext}"
1079
+ "${base}.spec.${ext}"
1080
+ "${dir}/__tests__/${filename}.test.${ext}"
1081
+ "${dir}/__tests__/${filename}.spec.${ext}"
1082
+ "tests/${filename}.test.${ext}"
1083
+ "tests/${filename}.spec.${ext}"
1084
+ )
1085
+
1086
+ for pattern in "${patterns[@]}"; do
1087
+ if [[ "$pattern" == *'*'* ]]; then
1088
+ found=$(find . -path "*/${pattern}" -type f 2>/dev/null | head -1)
1089
+ if [ -n "$found" ]; then TEST_FOUND=true; break; fi
1090
+ elif [ -f "$pattern" ]; then
1091
+ TEST_FOUND=true
1092
+ break
1093
+ fi
1094
+ done
1095
+
1096
+ if [ "$TEST_FOUND" = false ]; then
1097
+ if [ "$SKIP_5A_BLOCK" = true ] || [ "$TDD_BLOCK_DOWNGRADE" = true ]; then
1098
+ echo "⚠️ TEST PAIRING WARNING (downgraded): New TypeScript file without test: $src_file"
1099
+ echo " Expected: ${patterns[0]}, ${patterns[1]:-...}"
1100
+ echo " Or: Add '// @no-test-required: <reason>' if this file doesn't need tests"
1101
+ else
1102
+ echo "❌ BLOCKED: $src_file — New TypeScript source file without corresponding test"
1103
+ echo " Expected: ${patterns[0]}, ${patterns[1]:-...}"
1104
+ echo " Add '// @no-test-required: <reason>' if this file doesn't need tests"
1105
+ BLOCKED_TS_FILES=$((BLOCKED_TS_FILES + 1))
1106
+ fi
1107
+ fi
1108
+ done
1109
+
1110
+ if [ "$BLOCKED_TS_FILES" -gt 0 ]; then
1111
+ echo ""
1112
+ echo "❌ $BLOCKED_TS_FILES new TypeScript file(s) without corresponding tests — commit BLOCKED"
1113
+ echo " Add test files or '// @no-test-required: <reason>' annotation."
1114
+ echo " Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only)"
1115
+ exit 1
1116
+ fi
1117
+ fi
1118
+
1119
+ # --- WARNING path: Modified files (any lang) + New non-TS files ---
1120
+ WARNING_SOURCE_FILES=$(
1121
+ {
1122
+ git diff --cached --name-only --diff-filter=M | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
1123
+ git diff --cached --name-only --diff-filter=A | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
1124
+ } | sort -u || true
1125
+ )
1126
+
1127
+ if [ -n "$WARNING_SOURCE_FILES" ]; then
1059
1128
  PAIRING_WARNINGS=0
1060
- for src_file in $NEW_SOURCE_FILES; do
1129
+ for src_file in $WARNING_SOURCE_FILES; do
1061
1130
  base="${src_file%.*}"
1062
1131
  ext="${src_file##*.}"
1063
1132
  filename="${base##*/}"
@@ -1070,8 +1139,14 @@ else
1070
1139
  case "$ext" in
1071
1140
  d.ts|pyi) continue ;;
1072
1141
  esac
1073
- # Skip files with // @no-test annotation
1074
- if grep -q '@no-test' "$src_file" 2>/dev/null; then
1142
+ # Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
1143
+ # Order matters: @no-test-required must be checked first — @no-test is a substring match
1144
+ if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
1145
+ if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
1146
+ continue
1147
+ fi
1148
+ # @no-test-required with short reason (< 10 chars) — fall through to BLOCK
1149
+ elif grep -q '@no-test' "$src_file" 2>/dev/null; then
1075
1150
  continue
1076
1151
  fi
1077
1152
 
@@ -1149,18 +1224,18 @@ else
1149
1224
  done
1150
1225
 
1151
1226
  if [ "$TEST_FOUND" = false ]; then
1152
- echo "⚠️ TEST PAIRING WARNING: New source file without corresponding test: $src_file"
1227
+ echo "⚠️ TEST PAIRING WARNING: Source file without corresponding test: $src_file"
1153
1228
  echo " Expected patterns for .${ext}: ${patterns[0]}, ${patterns[1]:-...}"
1154
- echo " Or: Add '// @no-test' annotation if this file doesn't need tests"
1229
+ echo " Or: Add '// @no-test-required: <reason>' annotation if this file doesn't need tests"
1155
1230
  PAIRING_WARNINGS=$((PAIRING_WARNINGS + 1))
1156
1231
  fi
1157
1232
  done
1158
1233
 
1159
1234
  if [ "$PAIRING_WARNINGS" -gt 0 ]; then
1160
1235
  echo ""
1161
- echo "⚠️ $PAIRING_WARNINGS new source file(s) without corresponding tests"
1236
+ echo "⚠️ $PAIRING_WARNINGS source file(s) without corresponding tests"
1162
1237
  echo " This is a WARNING — commit proceeds. TDD order enforced by Agent skills."
1163
- echo " To suppress for specific files, add '// @no-test' at the top."
1238
+ echo " To suppress for specific files, add '// @no-test-required: <reason>' at the top."
1164
1239
  fi
1165
1240
  fi
1166
1241
 
@@ -1180,7 +1255,11 @@ else
1180
1255
  'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
1181
1256
  'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
1182
1257
  '\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
1183
- c=$(grep -c "$kw" "$test_file" 2>/dev/null)
1258
+ # Use grep -o piped to wc -l for reliable single-number output (grep -c can emit
1259
+ # multi-line output with filenames under some grep versions, causing bash arithmetic
1260
+ # syntax errors). grep -o extracts each match on its own line, wc -l counts them.
1261
+ c=$(grep -o "$kw" "$test_file" 2>/dev/null | wc -l || true)
1262
+ c=${c//[^0-9]/}
1184
1263
  c=${c:-0}
1185
1264
  MOCK_COUNT=$((MOCK_COUNT + c))
1186
1265
  done
@@ -1219,8 +1298,9 @@ else
1219
1298
  echo "Running tests..."
1220
1299
 
1221
1300
  # Route to appropriate test runner via adapter
1222
- if [ "${PROJECT_LANG}" != "unknown" ] && [ -f "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" ]; then
1223
- if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
1301
+ ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
1302
+ if [ "${PROJECT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
1303
+ if source "$ADAPTER_FILE" 2>/dev/null; then
1224
1304
  TESTS_OUTPUT=$(run_tests 2>&1)
1225
1305
  TESTS_EXIT_CODE=$?
1226
1306
  echo "$TESTS_OUTPUT" | head -50
@@ -1232,17 +1312,17 @@ else
1232
1312
  echo "✅ PASSED - Unit tests passed."
1233
1313
  fi
1234
1314
  else
1235
- echo "✅ PASSED - No adapter for tests, assuming none to run."
1315
+ echo "✅ PASSED - Could not source ${PROJECT_LANG} adapter for tests, assuming none to run."
1236
1316
  fi
1237
1317
  else
1238
- echo "✅ PASSED - No specific adapter, no tests to run."
1318
+ echo "✅ PASSED - No ${PROJECT_LANG} adapter found for tests, assuming none to run."
1239
1319
  fi
1240
1320
 
1241
1321
  # Next, run coverage check
1242
1322
  echo "Running coverage check..."
1243
1323
 
1244
- if [ "${PROJECT_LANG}" != "unknown" ] && [ -f "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" ]; then
1245
- if source "$ADAPTER_DIR/adapters/${PROJECT_LANG}.sh" 2>/dev/null; then
1324
+ if [ "${PROJECT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
1325
+ if source "$ADAPTER_FILE" 2>/dev/null; then
1246
1326
  case "$PROJECT_LANG" in
1247
1327
  "typescript")
1248
1328
  if [ -f "package.json" ] && grep -q '"test:coverage"' package.json 2>/dev/null; then
@@ -1299,12 +1379,12 @@ else
1299
1379
  esac
1300
1380
  else
1301
1381
  # Adapter source failed — warn and let Stage 2 attempt file-based enforcement
1302
- echo "⚠️ Could not source ${PROJECT_LANG} adapter, will check coverage files directly..."
1382
+ echo "⚠️ Could not source ${PROJECT_LANG} adapter from ${ADAPTER_FILE}, will check coverage files directly..."
1303
1383
  COV_EXIT=0
1304
1384
  fi
1305
1385
  else
1306
1386
  # No adapter available — warn and let Stage 2 attempt file-based enforcement
1307
- echo "⚠️ No adapter for ${PROJECT_LANG}, will check coverage files directly..."
1387
+ echo "⚠️ No adapter found for ${PROJECT_LANG} (searched: ${ADAPTER_DIR}/, ${ADAPTER_DIR}/adapters/, ${PROJECT_GITHOOKS}/adapters/, ${SCRIPT_DIR}/adapters/) — will check coverage files directly..."
1308
1388
  COV_EXIT=0
1309
1389
  fi
1310
1390
 
@@ -1488,7 +1568,7 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
1488
1568
 
1489
1569
  elif [ "$PROJECT_LANG" = "powershell" ]; then
1490
1570
  echo " ℹ️ No PowerShell architecture tooling available"
1491
- echo " Architecture validation (SKIP, no tool for PowerShell)"
1571
+ echo " ⏭️ SKIPPED - Architecture validation (no tool for PowerShell)"
1492
1572
 
1493
1573
  else
1494
1574
  # Check if architecture config exists
@@ -1549,7 +1629,7 @@ else
1549
1629
  fi
1550
1630
  else
1551
1631
  echo " ℹ️ No .import-linter.yml found - skipping Python architecture"
1552
- echo " Python architecture validation (SKIP, no config)"
1632
+ echo " ⏭️ SKIPPED - Python architecture validation (no config)"
1553
1633
  fi
1554
1634
  ;;
1555
1635
  "go")
@@ -1565,7 +1645,7 @@ else
1565
1645
  fi
1566
1646
  else
1567
1647
  echo " ℹ️ No arch-go.yaml found - skipping Go architecture"
1568
- echo " Go architecture validation (SKIP, no config)"
1648
+ echo " ⏭️ SKIPPED - Go architecture validation (no config)"
1569
1649
  fi
1570
1650
  ;;
1571
1651
  "java")
@@ -1581,19 +1661,19 @@ else
1581
1661
  echo " ✅ Java architecture validation completed."
1582
1662
  else
1583
1663
  echo " ℹ️ No architecture test files found - skipping"
1584
- echo " Architecture validation (SKIP, no tests)"
1664
+ echo " ⏭️ SKIPPED - Architecture validation (no tests)"
1585
1665
  fi
1586
1666
  ;;
1587
1667
  *)
1588
1668
  echo " ℹ️ Architecture validation not configured for $PROJECT_LANG"
1589
- echo " Architecture validation (SKIP, not configured)"
1669
+ echo " ⏭️ SKIPPED - Architecture validation (not configured for $PROJECT_LANG)"
1590
1670
  ;;
1591
1671
  esac
1592
1672
  else
1593
1673
  # Architecture config is REQUIRED — BLOCK if missing
1594
1674
  if [ -f ".architecture-skip" ]; then
1595
1675
  echo " ⚠️ Architecture config missing but .architecture-skip present — allowed to skip"
1596
- echo " Architecture validation (SKIP, .architecture-skip exemption)"
1676
+ echo " ⏭️ SKIPPED - Architecture validation (.architecture-skip exemption)"
1597
1677
  else
1598
1678
  echo ""
1599
1679
  echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
@@ -1626,8 +1706,15 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
1626
1706
  echo " ✅ Skipped (documentation project)."
1627
1707
 
1628
1708
  else
1629
- # Check if principles directory exists for Boy Scout
1630
- if [ -f "src/principles/boy-scout.ts" ]; then
1709
+ # Check for Boy Scout script in installed modules first, then project src/
1710
+ BOY_SCOUT_SCRIPT=""
1711
+ if [ -f ".xp-gate/modules/principles/boy-scout.ts" ]; then
1712
+ BOY_SCOUT_SCRIPT=".xp-gate/modules/principles/boy-scout.ts"
1713
+ elif [ -f "src/principles/boy-scout.ts" ]; then
1714
+ BOY_SCOUT_SCRIPT="src/principles/boy-scout.ts"
1715
+ fi
1716
+
1717
+ if [ -n "$BOY_SCOUT_SCRIPT" ]; then
1631
1718
  echo " Checking Boy Scout Rule compliance..."
1632
1719
 
1633
1720
  # Separate new files and modified files
@@ -1643,7 +1730,7 @@ else
1643
1730
  if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
1644
1731
  echo " ✅ No new or modified source files for Boy Scout check."
1645
1732
  else
1646
- BOY_SCOUT_OUTPUT=$(npx tsx src/principles/boy-scout.ts \
1733
+ BOY_SCOUT_OUTPUT=$(npx tsx $BOY_SCOUT_SCRIPT \
1647
1734
  --new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
1648
1735
  --modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
1649
1736
  --baseline ".warnings-baseline.json" 2>&1)
@@ -1675,12 +1762,12 @@ else
1675
1762
  fi
1676
1763
  else
1677
1764
  echo " ℹ️ npx not available - skipping Boy Scout Rule"
1678
- echo " Boy Scout Rule (SKIP, Node.js not available)"
1765
+ echo " ⏭️ SKIPPED - Boy Scout Rule (Node.js/npx not available)"
1679
1766
  fi
1680
1767
  fi
1681
1768
  else
1682
1769
  echo " ℹ️ Boy scout rule not available in project - skipping"
1683
- echo " Boy Scout Rule (SKIP, not available in project)"
1770
+ echo " ⏭️ SKIPPED - Boy Scout Rule (not available in project)"
1684
1771
  fi
1685
1772
  fi
1686
1773
  GATE_6_STATUS="PASS"
@@ -1690,105 +1777,15 @@ record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCK
1690
1777
 
1691
1778
  # ============================================================================
1692
1779
  # GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
1693
- # Detects security issues in Infrastructure as Code files
1694
- # Tools: checkov (recommended), hadolint, kube-score, tflint
1780
+ # Extracted to gate-7.sh for maintainability
1695
1781
  # ============================================================================
1696
- 2>&1 echo ""
1697
- 2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
1698
- GATE_7_START=$(gate_start_ms)
1699
-
1700
- # Check if any IaC files are changed
1701
- IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
1702
- if [ -z "$IAC_FILES" ]; then
1703
- echo "✅ PASSED - No IaC files detected in changes."
1704
- GATE_7_STATUS="PASS"
1705
- else
1706
- # Run IaC adapter
1707
- if [ -f "githooks/adapters/iac.sh" ]; then
1708
- # shellcheck source=githooks/adapters/iac.sh
1709
- source "githooks/adapters/iac.sh"
1710
-
1711
- # Run static analysis for IaC files
1712
- IAC_OUTPUT=$(run_static_analysis "$IAC_FILES" 2>&1)
1713
- IAC_EXIT=$?
1714
-
1715
- echo "$IAC_OUTPUT"
1716
-
1717
- if [ $IAC_EXIT -eq 0 ]; then
1718
- echo "✅ PASSED - IaC security scan."
1719
- GATE_7_STATUS="PASS"
1720
- else
1721
- echo ""
1722
- echo "❌ BLOCKED - IaC security issues detected"
1723
- echo "Fix the security issues above before committing."
1724
- echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
1725
- GATE_7_STATUS="FAIL"
1726
- exit 1
1727
- fi
1728
- else
1729
- echo "ℹ️ SKIP - IaC adapter not found"
1730
- echo "✅ PASSED - IaC Security (SKIP)"
1731
- GATE_7_STATUS="SKIP"
1732
- fi
1733
- fi
1734
- record_gate_audit "gate-7" "iac-security" "$GATE_7_STATUS" "0" "$GATE_7_START"
1782
+ source "$GATE_DIR/gate-7.sh"
1735
1783
 
1736
1784
 
1737
1785
  # GATE 8: Secret Scanning (gitleaks)
1738
- # Detects secrets (API keys, passwords, tokens) in staged files
1739
- # Tool: gitleaks -- https://github.com/gitleaks/gitleaks
1786
+ # Extracted to gate-8.sh for maintainability
1740
1787
  # ============================================================================
1741
-
1742
- 2>&1 echo ""
1743
- 2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
1744
- GATE_8_START=$(gate_start_ms)
1745
-
1746
- # Gitleaks availability check
1747
- GITLEAKS_CMD=""
1748
- if command -v gitleaks >/dev/null 2>&1; then
1749
- GITLEAKS_CMD="gitleaks"
1750
- elif [ -f "$HOME/.local/bin/gitleaks" ]; then
1751
- GIBLEAKS_CMD="$HOME/.local/bin/gitleaks"
1752
- fi
1753
-
1754
- if [ -n "$GITLEAKS_CMD" ]; then
1755
- GITLEAKS_CONFIG=""
1756
- if [ -f ".gitleaks.toml" ]; then
1757
- GITLEAKS_CONFIG="--config=.gitleaks.toml"
1758
- fi
1759
-
1760
- # Run gitleaks on staged changes only (pre-commit mode for speed)
1761
- GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --pre-commit --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path=/tmp/gitleaks-report.json 2>&1)
1762
- GITLEAKS_EXIT=$?
1763
-
1764
- if [ "$GITLEAKS_EXIT" -eq 0 ]; then
1765
- echo " ✅ PASSED - No secrets detected."
1766
- GATE_8_STATUS="PASS"
1767
- elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
1768
- # Secrets found — output details
1769
- echo "$GITLEAKS_OUTPUT"
1770
- echo ""
1771
- echo "❌ BLOCKED - Secrets detected in staged files."
1772
- echo ""
1773
- echo "Remediation options:"
1774
- echo " 1. Remove the secret and use environment variables instead"
1775
- echo " 2. Add a false positive to .gitleaks.toml allowlist"
1776
- echo " 3. Use git secret or vault for sensitive data"
1777
- echo ""
1778
- echo "See: https://github.com/gitleaks/gitleaks"
1779
- exit 1
1780
- else
1781
- echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
1782
- echo " ✅ Secret Scanning (SKIP, gitleaks error)"
1783
- GATE_8_STATUS="SKIP"
1784
- fi
1785
- else
1786
- echo " ℹ️ gitleaks not installed — secret scanning unavailable"
1787
- echo " Install: brew install gitleaks (macOS) | scripts/install-gitleaks.sh (Linux)"
1788
- echo " ✅ Secret Scanning (SKIP, gitleaks not installed)"
1789
- GATE_8_STATUS="SKIP"
1790
- fi
1791
- record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
1788
+ source "$GATE_DIR/gate-8.sh"
1792
1789
 
1793
1790
  # Switch back to original directory if we were in a subdirectory
1794
1791
  if [ -n "$ORIGINAL_DIR" ]; then
@@ -1797,134 +1794,42 @@ fi
1797
1794
 
1798
1795
  # ============================================================================
1799
1796
  # GATE 9: Semgrep SAST Security Scan
1800
- # Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
1801
- # Tool: semgrep -- https://semgrep.dev
1797
+ # Extracted to gate-9.sh for maintainability
1798
+ # ============================================================================
1799
+ source "$GATE_DIR/gate-9.sh"
1802
1800
  # ============================================================================
1803
1801
 
1804
- 2>&1 echo ""
1805
- 2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
1806
- GATE_9_START=$(gate_start_ms)
1807
-
1808
- GATE_9_STATUS="PASS"
1809
-
1810
- # Semgrep availability check
1811
- SEMGREP_CMD=""
1812
- if command -v semgrep >/dev/null 2>&1; then
1813
- SEMGREP_CMD="semgrep"
1814
- elif [ -f "$HOME/.local/bin/semgrep" ]; then
1815
- SEMGREP_CMD="$HOME/.local/bin/semgrep"
1802
+ # ============================================================================
1803
+ # GATE 10: Sprint Flow Enforcement
1804
+ # Validates sprint state consistency (delphi-review APPROVED before BUILD)
1805
+ # Uses sprint-gate.sh for standalone validation logic
1806
+ # ============================================================================
1807
+ GATE_10_START=$(date +%s)
1808
+ GATE_10_STATUS="PASS"
1809
+
1810
+ SPRINT_GATE_SCRIPT=""
1811
+ if [ -f "$GATE_DIR/sprint-gate.sh" ]; then
1812
+ SPRINT_GATE_SCRIPT="$GATE_DIR/sprint-gate.sh"
1813
+ elif [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/sprint-gate.sh" ]; then
1814
+ SPRINT_GATE_SCRIPT="$(git rev-parse --show-toplevel)/githooks/sprint-gate.sh"
1816
1815
  fi
1817
1816
 
1818
- if [ -z "$SEMGREP_CMD" ]; then
1819
- echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
1820
- echo " Install: brew install semgrep | pip install semgrep"
1821
- echo " Pre-cache rules: semgrep --config=p/security-audit"
1822
- echo " Gate 9: SAST Security (WARN, semgrep not installed)"
1823
- GATE_9_STATUS="WARN"
1824
- else
1825
- # Get staged files filtered to Semgrep-supported languages
1826
- SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
1827
-
1828
- if [ -z "$SEMGREP_FILES" ]; then
1829
- echo " ✅ PASSED - No supported language files in staged changes."
1830
- GATE_9_STATUS="PASS"
1831
- else
1832
- # Run semgrep with JSON output
1833
- # --config=p/security-audit: explicit security ruleset
1834
- # --json: machine-readable output
1835
- # --disable-version-check: skip network call
1836
- SEMGREP_OUTPUT=$($SEMGREP_CMD scan --config=p/security-audit --json --disable-version-check $SEMGREP_FILES 2>&1)
1837
- SEMGREP_EXIT=$?
1838
-
1839
- if [ "$SEMGREP_EXIT" -eq 0 ]; then
1840
- echo " ✅ PASSED - No security vulnerabilities found."
1841
- GATE_9_STATUS="PASS"
1842
- elif [ "$SEMGREP_EXIT" -eq 1 ]; then
1843
- # Findings detected - parse JSON to categorize
1844
- CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1845
- import sys, json
1846
- try:
1847
- data = json.load(sys.stdin)
1848
- results = data.get('results', [])
1849
- count = 0
1850
- for r in results:
1851
- extra = r.get('extra', {})
1852
- severity = extra.get('severity', '').upper()
1853
- if severity in ('CRITICAL', 'HIGH'):
1854
- count += 1
1855
- print(count)
1856
- except:
1857
- print('0')
1858
- " 2>/dev/null || echo "0")
1859
-
1860
- MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1861
- import sys, json
1862
- try:
1863
- data = json.load(sys.stdin)
1864
- results = data.get('results', [])
1865
- count = 0
1866
- for r in results:
1867
- extra = r.get('extra', {})
1868
- severity = extra.get('severity', '').upper()
1869
- if severity in ('MEDIUM', 'LOW'):
1870
- count += 1
1871
- print(count)
1872
- except:
1873
- print('0')
1874
- " 2>/dev/null || echo "0")
1875
-
1876
- # Extract top finding details
1877
- FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | python3 -c "
1878
- import sys, json
1879
- try:
1880
- data = json.load(sys.stdin)
1881
- results = data.get('results', [])
1882
- for r in results[:5]: # Show top 5
1883
- extra = r.get('extra', {})
1884
- severity = extra.get('severity', 'UNKNOWN').upper()
1885
- rule_id = r.get('check_id', 'unknown')
1886
- path = r.get('path', 'unknown')
1887
- start_line = r.get('start', {}).get('line', '?')
1888
- message = extra.get('message', '')[:80]
1889
- print(f' [{severity}] {rule_id}')
1890
- print(f' {path}:{start_line} → {message}')
1891
- print()
1892
- except:
1893
- print(' (Failed to parse semgrep output)')
1894
- " 2>/dev/null || echo " (Failed to parse semgrep output)")
1895
-
1896
- if [ "$CRITICAL_HIGH" -gt 0 ]; then
1897
- echo ""
1898
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1899
- echo " GATE 9: Semgrep Security Gate"
1900
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1901
- echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
1902
- echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
1903
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1904
- echo " ❌ BLOCKED — Critical/High vulnerability found"
1905
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
1906
- echo "$FINDING_DETAILS"
1907
- echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
1908
- GATE_9_STATUS="FAIL"
1909
- exit 1
1910
- else
1911
- echo ""
1912
- echo " ✅ PASSED - No critical/high vulnerabilities"
1913
- if [ "$MEDIUM_LOW" -gt 0 ]; then
1914
- echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
1915
- echo "$FINDING_DETAILS"
1916
- fi
1917
- GATE_9_STATUS="PASS"
1918
- fi
1919
- else
1920
- # semgrep runtime error (timeout, config error, etc.)
1921
- echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
1922
- echo " ✅ Semgrep SAST (SKIP, semgrep error)"
1923
- GATE_9_STATUS="SKIP"
1924
- fi
1817
+ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
1818
+ if ! bash "$SPRINT_GATE_SCRIPT" --pre-commit; then
1819
+ GATE_10_STATUS="BLOCK"
1820
+ echo "❌ BLOCKED - Gate 10: Sprint Flow Enforcement"
1821
+ # Sprint gate failure is a hard block — exit immediately
1822
+ exit 1
1925
1823
  fi
1824
+ else
1825
+ echo "⏭️ SKIPPED - Gate 10: Sprint Flow (sprint-gate.sh not found)"
1826
+ GATE_10_STATUS="SKIP"
1926
1827
  fi
1927
- record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
1828
+
1829
+ GATE_10_END=$(date +%s)
1830
+ GATE_10_DURATION=$((GATE_10_END - GATE_10_START))
1831
+ echo "✅ Gate 10 completed in ${GATE_10_DURATION}s"
1832
+ echo ""
1928
1833
  # ============================================================================
1929
1834
 
1930
1835
  generate_quality_report() {
@@ -1933,13 +1838,13 @@ generate_quality_report() {
1933
1838
  local TIMESTAMP
1934
1839
  local BRANCH
1935
1840
  local PASSED_COUNT=0
1936
- local TOTAL_GATES=9
1841
+ local TOTAL_GATES=10
1937
1842
 
1938
1843
  COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
1939
1844
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
1940
1845
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1941
1846
 
1942
- for gate in 1 2 3 4 5 6 7 8 9; do
1847
+ for gate in 1 2 3 4 5 6 7 8 9 10; do
1943
1848
  local status_var="GATE_${gate}_STATUS"
1944
1849
  if [ "${!status_var}" = "PASS" ]; then
1945
1850
  PASSED_COUNT=$((PASSED_COUNT + 1))
@@ -2042,6 +1947,11 @@ generate_quality_report() {
2042
1947
  "name": "SAST Security Scan",
2043
1948
  "status": "${GATE_9_STATUS:-PASS}",
2044
1949
  "tool": "semgrep"
1950
+ },
1951
+ "gate10_sprint_flow": {
1952
+ "name": "Sprint Flow Enforcement",
1953
+ "status": "${GATE_10_STATUS:-PASS}",
1954
+ "tool": "sprint-gate.sh"
2045
1955
  }
2046
1956
  }
2047
1957
  }
@@ -2082,7 +1992,8 @@ PY
2082
1992
  GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-PASS}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
2083
1993
  GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-PASS}\",\"name\":\"IaC Security\"},"
2084
1994
  GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
2085
- GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"SAST Security\"}}"
1995
+ GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"SAST Security\"},"
1996
+ GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-PASS}\",\"name\":\"Sprint Flow\"}}"
2086
1997
  GATES_JSON="${GATES_JSON}}"
2087
1998
 
2088
1999
  echo "{\"timestamp\":\"$TIMESTAMP\",\"commit\":\"$COMMIT_HASH\",\"branch\":\"$BRANCH\",\"score\":$SCORE,\"passed\":$PASSED_COUNT,\"total\":$TOTAL_GATES,\"gates\":$GATES_JSON,\"coverage\":\"$COV_PCT\",\"complexityWarnings\":${CC_WARNINGS:-0},\"principleWarnings\":${WARNING_COUNT:-0},\"boyScoutBlocked\":${BS_BLOCKED}}" >> "$HISTORY_FILE"
@@ -2102,6 +2013,7 @@ PY
2102
2013
  printf " %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-PASS}"
2103
2014
  printf " %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
2104
2015
  printf " %-45s %s\n" "Gate 9: SAST Security" "${GATE_9_STATUS:-PASS}"
2016
+ printf " %-45s %s\n" "Gate 10: Sprint Flow" "${GATE_10_STATUS:-PASS}"
2105
2017
  echo ""
2106
2018
  echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
2107
2019
  echo " Branch status: $REPORT_FILE"