@boyingliu01/xp-gate 0.18.1 → 0.18.3
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/adapters/gate-3.sh +68 -0
- package/adapters/gate-4.sh +80 -0
- package/adapters/gate-7.sh +45 -0
- package/adapters/gate-8.sh +56 -0
- package/adapters/gate-9.sh +105 -0
- package/bin/xp-gate.js +45 -0
- package/hooks/pre-commit +108 -9
- package/hooks/pre-push +63 -232
- package/lib/__tests__/install-skill.test.js +3 -3
- package/lib/__tests__/test-alignment.test.ts +761 -0
- package/lib/install-skill.js +11 -3
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/phase-transition.js +106 -1
- package/lib/sprint-init.js +41 -0
- package/lib/test-alignment.ts +651 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/gate-m.ts +18 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# GATE 3: Cyclomatic Complexity Check
|
|
3
|
+
# Uses lizard - checks cyclomatic complexity of changed source files
|
|
4
|
+
# ============================================================================
|
|
5
|
+
|
|
6
|
+
2>&1 echo ""
|
|
7
|
+
2>&1 echo "→ Gate 3: Cyclomatic complexity..."
|
|
8
|
+
GATE_3_START=$(gate_start_ms)
|
|
9
|
+
|
|
10
|
+
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
11
|
+
echo "⏭️ SKIPPED - Complexity (documentation project)."
|
|
12
|
+
GATE_3_STATUS="SKIP"
|
|
13
|
+
|
|
14
|
+
elif [ "$PROJECT_LANG" = "powershell" ]; then
|
|
15
|
+
echo "ℹ️ No PowerShell Clean Code / SOLID tool available"
|
|
16
|
+
echo "⏭️ SKIPPED - Complexity (no PowerShell tool)"
|
|
17
|
+
GATE_3_STATUS="SKIP"
|
|
18
|
+
|
|
19
|
+
else
|
|
20
|
+
CCN_THRESHOLD=5
|
|
21
|
+
|
|
22
|
+
# Check lizard availability
|
|
23
|
+
LIZARD_CMD=""
|
|
24
|
+
if command -v lizard > /dev/null 2>&1; then
|
|
25
|
+
LIZARD_CMD=lizard
|
|
26
|
+
elif [ -f ~/.local/bin/lizard ]; then
|
|
27
|
+
LIZARD_CMD=~/.local/bin/lizard
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [ -n "$LIZARD_CMD" ]; then
|
|
31
|
+
LIZARD_PATH=$(eval echo "$LIZARD_CMD")
|
|
32
|
+
|
|
33
|
+
# Get changed source files for complexity check (includes test files)
|
|
34
|
+
CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
|
|
35
|
+
|
|
36
|
+
if [ -n "$CC_FILES" ]; then
|
|
37
|
+
echo "Checking complexity for source files..."
|
|
38
|
+
|
|
39
|
+
# Run lizard with CCN threshold
|
|
40
|
+
CC_OUTPUT=$($LIZARD_PATH -C $CCN_THRESHOLD $CC_FILES 2>&1 || true)
|
|
41
|
+
|
|
42
|
+
# Parse warning count from the summary table: "Warning cnt 8"
|
|
43
|
+
# Use anchored grep to avoid matching lizard table headers (e.g. "Rt" column)
|
|
44
|
+
CC_WARNINGS=$(echo "$CC_OUTPUT" | grep "^Warning cnt" | awk '{print $NF}' | tr -d '[:space:]' | sed 's/[^0-9]//g' || true)
|
|
45
|
+
CC_WARNINGS=${CC_WARNINGS:-0}
|
|
46
|
+
|
|
47
|
+
if [ "$CC_WARNINGS" -gt 0 ]; then
|
|
48
|
+
echo "$CC_OUTPUT"
|
|
49
|
+
echo ""
|
|
50
|
+
echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
|
|
51
|
+
echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
|
|
52
|
+
exit 1
|
|
53
|
+
else
|
|
54
|
+
echo "✅ PASSED - All functions within complexity threshold ($CCN_THRESHOLD)."
|
|
55
|
+
GATE_3_STATUS="PASS"
|
|
56
|
+
fi
|
|
57
|
+
else
|
|
58
|
+
echo "⏭️ SKIPPED - Complexity (no source files to check)."
|
|
59
|
+
GATE_3_STATUS="SKIP"
|
|
60
|
+
fi
|
|
61
|
+
else
|
|
62
|
+
echo "⚠️ WARN - lizard not installed, complexity check not performed"
|
|
63
|
+
echo " Install with: pip install --user lizard"
|
|
64
|
+
echo " Gate 3: Complexity check (WARN, tool not available)"
|
|
65
|
+
GATE_3_STATUS="WARN"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
record_gate_audit "gate-3" "complexity" "$GATE_3_STATUS" "${CC_WARNINGS:-0}" "$GATE_3_START"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# GATE 4: Principles Checker (Clean Code + SOLID)
|
|
3
|
+
# Reuses existing principles checker logic
|
|
4
|
+
# ============================================================================
|
|
5
|
+
|
|
6
|
+
2>&1 echo ""
|
|
7
|
+
2>&1 echo "→ Gate 4: Principles checker (Clean Code + SOLID)..."
|
|
8
|
+
GATE_4_START=$(gate_start_ms)
|
|
9
|
+
|
|
10
|
+
GATE_4_STATUS=""
|
|
11
|
+
|
|
12
|
+
if [ "$PROJECT_LANG" = "documentation-only" ]; then
|
|
13
|
+
echo "⏭️ SKIPPED - Principles check (documentation project)."
|
|
14
|
+
GATE_4_STATUS="SKIP"
|
|
15
|
+
|
|
16
|
+
else
|
|
17
|
+
# Get source files to check against principles
|
|
18
|
+
PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
|
|
19
|
+
|
|
20
|
+
if [ -n "$PRINCIPLES_FILES" ]; then
|
|
21
|
+
# Check for principles checker in installed modules first, then project src/
|
|
22
|
+
PRINCIPLES_DIR=""
|
|
23
|
+
if [ -d ".xp-gate/modules/principles" ]; then
|
|
24
|
+
PRINCIPLES_DIR=".xp-gate/modules/principles"
|
|
25
|
+
elif [ -f "src/principles/index.ts" ]; then
|
|
26
|
+
PRINCIPLES_DIR="src/principles"
|
|
27
|
+
elif [ -d "$HOME/.config/xp-gate/modules/principles" ]; then
|
|
28
|
+
PRINCIPLES_DIR="$HOME/.config/xp-gate/modules/principles"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [ -n "$PRINCIPLES_DIR" ]; then
|
|
32
|
+
echo "Checking Clean Code + SOLID principles..."
|
|
33
|
+
|
|
34
|
+
if command -v npx > /dev/null 2>&1; then
|
|
35
|
+
# Run principles checker and store results
|
|
36
|
+
if npx tsx $PRINCIPLES_DIR/index.ts --files $PRINCIPLES_FILES --format json > /tmp/principles-output.json 2>/dev/null; then
|
|
37
|
+
# Check severity levels
|
|
38
|
+
ERROR_COUNT=$(grep -c '"severity":"error"' /tmp/principles-output.json 2>/dev/null || true)
|
|
39
|
+
ERROR_COUNT=${ERROR_COUNT:-0}
|
|
40
|
+
WARNING_COUNT=$(grep -c '"severity":"warning"' /tmp/principles-output.json 2>/dev/null || true)
|
|
41
|
+
WARNING_COUNT=${WARNING_COUNT:-0}
|
|
42
|
+
|
|
43
|
+
if [ "$ERROR_COUNT" -gt 0 ]; then
|
|
44
|
+
echo ""
|
|
45
|
+
echo "❌ BLOCKED - $ERROR_COUNT principle ERROR(S) found"
|
|
46
|
+
echo "Critical violations must be fixed before commit:"
|
|
47
|
+
echo " - error-handling violations"
|
|
48
|
+
echo " - SOLID principle violations"
|
|
49
|
+
echo " - architectural violations"
|
|
50
|
+
npx tsx $PRINCIPLES_DIR/index.ts --files $PRINCIPLES_FILES --format console
|
|
51
|
+
GATE_4_STATUS="FAIL"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
echo "✅ PASSED - Principles checker (no errors found)."
|
|
56
|
+
GATE_4_STATUS="PASS"
|
|
57
|
+
if [ "$WARNING_COUNT" -gt 0 ]; then
|
|
58
|
+
echo "ℹ️ $WARNING_COUNT warnings found (will be handled by Boy Scout Rule)."
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
echo "⚠️ Warning: Principles checker execution failed"
|
|
62
|
+
echo "⏭️ SKIPPED - Principles check (execution issue)"
|
|
63
|
+
GATE_4_STATUS="SKIP"
|
|
64
|
+
fi
|
|
65
|
+
else
|
|
66
|
+
echo "ℹ️ npx not available - skipping principles check"
|
|
67
|
+
echo "⏭️ SKIPPED - Principles check (no Node.js/npx)"
|
|
68
|
+
GATE_4_STATUS="SKIP"
|
|
69
|
+
fi
|
|
70
|
+
else
|
|
71
|
+
echo "ℹ️ Principles checker not found in project - skipping"
|
|
72
|
+
echo "⏭️ SKIPPED - Principles check (checker not in project)"
|
|
73
|
+
GATE_4_STATUS="SKIP"
|
|
74
|
+
fi
|
|
75
|
+
else
|
|
76
|
+
echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
|
|
77
|
+
GATE_4_STATUS="SKIP"
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
record_gate_audit "gate-4" "principles" "$GATE_4_STATUS" "${WARNING_COUNT:-0}" "$GATE_4_START"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
|
|
3
|
+
# Detects security issues in Infrastructure as Code files
|
|
4
|
+
# Tools: checkov (recommended), hadolint, kube-score, tflint
|
|
5
|
+
# ============================================================================
|
|
6
|
+
|
|
7
|
+
2>&1 echo ""
|
|
8
|
+
2>&1 echo "→ Gate 7: IaC Security Scanning (Terraform, Kubernetes, Docker)..."
|
|
9
|
+
GATE_7_START=$(gate_start_ms)
|
|
10
|
+
|
|
11
|
+
# Check if any IaC files are changed
|
|
12
|
+
IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
|
|
13
|
+
if [ -z "$IAC_FILES" ]; then
|
|
14
|
+
echo "✅ PASSED - No IaC files detected in changes."
|
|
15
|
+
GATE_7_STATUS="PASS"
|
|
16
|
+
else
|
|
17
|
+
# Run IaC adapter
|
|
18
|
+
if [ -f "githooks/adapters/iac.sh" ]; then
|
|
19
|
+
# shellcheck source=githooks/adapters/iac.sh
|
|
20
|
+
source "githooks/adapters/iac.sh"
|
|
21
|
+
|
|
22
|
+
# Run static analysis for IaC files
|
|
23
|
+
IAC_OUTPUT=$(run_static_analysis "$IAC_FILES" 2>&1)
|
|
24
|
+
IAC_EXIT=$?
|
|
25
|
+
|
|
26
|
+
echo "$IAC_OUTPUT"
|
|
27
|
+
|
|
28
|
+
if [ $IAC_EXIT -eq 0 ]; then
|
|
29
|
+
echo "✅ PASSED - IaC security scan."
|
|
30
|
+
GATE_7_STATUS="PASS"
|
|
31
|
+
else
|
|
32
|
+
echo ""
|
|
33
|
+
echo "❌ BLOCKED - IaC security issues detected"
|
|
34
|
+
echo "Fix the security issues above before committing."
|
|
35
|
+
echo "Tip: Install checkov for comprehensive IaC scanning: pip install checkov"
|
|
36
|
+
GATE_7_STATUS="FAIL"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
else
|
|
40
|
+
echo "ℹ️ SKIP - IaC adapter not found"
|
|
41
|
+
echo "⏭️ SKIPPED - IaC security (no IaC adapter found)"
|
|
42
|
+
GATE_7_STATUS="SKIP"
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
record_gate_audit "gate-7" "iac-security" "$GATE_7_STATUS" "0" "$GATE_7_START"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# GATE 8: Secret Scanning (gitleaks)
|
|
3
|
+
# Detects secrets (API keys, passwords, tokens) in staged files
|
|
4
|
+
# Tool: gitleaks -- https://github.com/gitleaks/gitleaks
|
|
5
|
+
# ============================================================================
|
|
6
|
+
|
|
7
|
+
2>&1 echo ""
|
|
8
|
+
2>&1 echo "→ Gate 8: Secret scanning (gitleaks)..."
|
|
9
|
+
GATE_8_START=$(gate_start_ms)
|
|
10
|
+
|
|
11
|
+
# Gitleaks availability check
|
|
12
|
+
GITLEAKS_CMD=""
|
|
13
|
+
if command -v gitleaks >/dev/null 2>&1; then
|
|
14
|
+
GITLEAKS_CMD="gitleaks"
|
|
15
|
+
elif [ -f "$HOME/.local/bin/gitleaks" ]; then
|
|
16
|
+
GITLEAKS_CMD="$HOME/.local/bin/gitleaks"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [ -n "$GITLEAKS_CMD" ]; then
|
|
20
|
+
GITLEAKS_CONFIG=""
|
|
21
|
+
if [ -f ".gitleaks.toml" ]; then
|
|
22
|
+
GITLEAKS_CONFIG="--config=.gitleaks.toml"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Run gitleaks on staged changes only (pre-commit mode for speed)
|
|
26
|
+
GITLEAKS_OUTPUT=$($GITLEAKS_CMD git --pre-commit --redact --no-banner $GITLEAKS_CONFIG --report-format=json --report-path=/tmp/gitleaks-report.json 2>&1)
|
|
27
|
+
GITLEAKS_EXIT=$?
|
|
28
|
+
|
|
29
|
+
if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
30
|
+
echo " ✅ PASSED - No secrets detected."
|
|
31
|
+
GATE_8_STATUS="PASS"
|
|
32
|
+
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
33
|
+
# Secrets found — output details
|
|
34
|
+
echo "$GITLEAKS_OUTPUT"
|
|
35
|
+
echo ""
|
|
36
|
+
echo "❌ BLOCKED - Secrets detected in staged files."
|
|
37
|
+
echo ""
|
|
38
|
+
echo "Remediation options:"
|
|
39
|
+
echo " 1. Remove the secret and use environment variables instead"
|
|
40
|
+
echo " 2. Add a false positive to .gitleaks.toml allowlist"
|
|
41
|
+
echo " 3. Use git secret or vault for sensitive data"
|
|
42
|
+
echo ""
|
|
43
|
+
echo "See: https://github.com/gitleaks/gitleaks"
|
|
44
|
+
exit 1
|
|
45
|
+
else
|
|
46
|
+
echo " ⚠️ gitleaks exited with code $GITLEAKS_EXIT - skipping gate"
|
|
47
|
+
echo " ✅ Secret Scanning (SKIP, gitleaks error)"
|
|
48
|
+
GATE_8_STATUS="SKIP"
|
|
49
|
+
fi
|
|
50
|
+
else
|
|
51
|
+
echo " ℹ️ gitleaks not installed — secret scanning unavailable"
|
|
52
|
+
echo " Install: brew install gitleaks (macOS) | winget install gitleaks (Windows) | scripts/install-gitleaks.sh (Linux)"
|
|
53
|
+
echo " ⏭️ SKIPPED - Secret scanning (gitleaks not installed)"
|
|
54
|
+
GATE_8_STATUS="SKIP"
|
|
55
|
+
fi
|
|
56
|
+
record_gate_audit "gate-8" "secret-scanning" "$GATE_8_STATUS" "0" "$GATE_8_START"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# GATE 9: Semgrep SAST Security Scan
|
|
3
|
+
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
4
|
+
# Tool: semgrep -- https://semgrep.dev
|
|
5
|
+
# ============================================================================
|
|
6
|
+
|
|
7
|
+
2>&1 echo ""
|
|
8
|
+
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
9
|
+
GATE_9_START=$(gate_start_ms)
|
|
10
|
+
|
|
11
|
+
GATE_9_STATUS=""
|
|
12
|
+
|
|
13
|
+
# Semgrep availability check
|
|
14
|
+
SEMGREP_CMD=""
|
|
15
|
+
if command -v semgrep >/dev/null 2>&1; then
|
|
16
|
+
SEMGREP_CMD="semgrep"
|
|
17
|
+
elif [ -f "$HOME/.local/bin/semgrep" ]; then
|
|
18
|
+
SEMGREP_CMD="$HOME/.local/bin/semgrep"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if [ -z "$SEMGREP_CMD" ]; then
|
|
22
|
+
echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
|
|
23
|
+
echo " Install: brew install semgrep (macOS) | pip install semgrep (Linux) | pip install semgrep (Windows)"
|
|
24
|
+
echo " Pre-cache rules: semgrep --config=p/security-audit"
|
|
25
|
+
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
26
|
+
GATE_9_STATUS="WARN"
|
|
27
|
+
else
|
|
28
|
+
# Get staged files filtered to Semgrep-supported languages
|
|
29
|
+
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)
|
|
30
|
+
|
|
31
|
+
if [ -z "$SEMGREP_FILES" ]; then
|
|
32
|
+
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
33
|
+
GATE_9_STATUS="SKIP"
|
|
34
|
+
else
|
|
35
|
+
# Run semgrep with JSON output
|
|
36
|
+
# --config=p/security-audit: explicit security ruleset
|
|
37
|
+
# --json: machine-readable output
|
|
38
|
+
# --disable-version-check: skip network call
|
|
39
|
+
SEMGREP_OUTPUT=$($SEMGREP_CMD scan --config=p/security-audit --json --disable-version-check $SEMGREP_FILES 2>&1)
|
|
40
|
+
SEMGREP_EXIT=$?
|
|
41
|
+
|
|
42
|
+
if [ "$SEMGREP_EXIT" -eq 0 ]; then
|
|
43
|
+
echo " ✅ PASSED - No security vulnerabilities found."
|
|
44
|
+
GATE_9_STATUS="PASS"
|
|
45
|
+
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
46
|
+
# Findings detected - parse JSON to categorize
|
|
47
|
+
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
48
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
49
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
50
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='CRITICAL'||s==='HIGH';}).length;
|
|
51
|
+
console.log(count);}catch(e){console.log('0');}
|
|
52
|
+
});" 2>/dev/null || echo "0")
|
|
53
|
+
|
|
54
|
+
MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
55
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
56
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
57
|
+
const count=results.filter(r=>{const s=(r.extra&&r.extra.severity||'').toUpperCase();return s==='MEDIUM'||s==='LOW';}).length;
|
|
58
|
+
console.log(count);}catch(e){console.log('0');}
|
|
59
|
+
});" 2>/dev/null || echo "0")
|
|
60
|
+
|
|
61
|
+
# Extract top finding details
|
|
62
|
+
FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | node -e "
|
|
63
|
+
let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
|
|
64
|
+
try{const data=JSON.parse(d);const results=data.results||[];
|
|
65
|
+
results.slice(0,5).forEach(r=>{const extra=r.extra||{};
|
|
66
|
+
const severity=(extra.severity||'UNKNOWN').toUpperCase();
|
|
67
|
+
const ruleId=r.check_id||'unknown';const path=r.path||'unknown';
|
|
68
|
+
const line=(r.start&&r.start.line)||'?';const msg=(extra.message||'').slice(0,80);
|
|
69
|
+
console.log(' ['+severity+'] '+ruleId);console.log(' '+path+':'+line+' → '+msg);console.log();});
|
|
70
|
+
}catch(e){console.log(' (Failed to parse semgrep output)');}
|
|
71
|
+
});" 2>/dev/null || echo " (Failed to parse semgrep output)")
|
|
72
|
+
|
|
73
|
+
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
74
|
+
echo ""
|
|
75
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
76
|
+
echo " GATE 9: Semgrep Security Gate"
|
|
77
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
78
|
+
echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
|
|
79
|
+
echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
|
|
80
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
81
|
+
echo " ❌ BLOCKED — Critical/High vulnerability found"
|
|
82
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
83
|
+
echo "$FINDING_DETAILS"
|
|
84
|
+
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
85
|
+
GATE_9_STATUS="FAIL"
|
|
86
|
+
exit 1
|
|
87
|
+
else
|
|
88
|
+
echo ""
|
|
89
|
+
echo " ✅ PASSED - No critical/high vulnerabilities"
|
|
90
|
+
if [ "$MEDIUM_LOW" -gt 0 ]; then
|
|
91
|
+
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
92
|
+
echo "$FINDING_DETAILS"
|
|
93
|
+
fi
|
|
94
|
+
GATE_9_STATUS="PASS"
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
# semgrep runtime error (timeout, config error, etc.)
|
|
98
|
+
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
99
|
+
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
100
|
+
GATE_9_STATUS="SKIP"
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
105
|
+
|
package/bin/xp-gate.js
CHANGED
|
@@ -28,6 +28,46 @@ function handleUIReview() {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
async function runAlignmentCheck(subargs) {
|
|
32
|
+
const specIdx = subargs.indexOf('--spec');
|
|
33
|
+
const specPath = specIdx >= 0 ? subargs[specIdx + 1] : path.join(process.cwd(), 'specification.yaml');
|
|
34
|
+
|
|
35
|
+
const testsIdx = subargs.indexOf('--tests');
|
|
36
|
+
const testDirs = testsIdx >= 0 ? [subargs[testsIdx + 1]] : undefined;
|
|
37
|
+
|
|
38
|
+
const outputIdx = subargs.indexOf('--output');
|
|
39
|
+
const outputPath = outputIdx >= 0 ? subargs[outputIdx + 1] : undefined;
|
|
40
|
+
|
|
41
|
+
const jsonMode = subargs.includes('--json');
|
|
42
|
+
|
|
43
|
+
// Load TS module via tsx
|
|
44
|
+
const { execSync } = require('child_process');
|
|
45
|
+
const alignmentPath = path.join(__dirname, '..', 'lib', 'test-alignment.ts');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
execSync(`npx tsx -e "
|
|
49
|
+
import { runFullAlignment } from '${alignmentPath}';
|
|
50
|
+
const report = runFullAlignment({
|
|
51
|
+
specPath: '${specPath}',
|
|
52
|
+
testDirs: ${JSON.stringify(testDirs || null)},
|
|
53
|
+
headCommit: require('child_process').execSync('git rev-parse HEAD 2>/dev/null || echo unknown').toString().trim(),
|
|
54
|
+
outputPath: ${outputPath ? `'${outputPath}'` : 'undefined'},
|
|
55
|
+
});
|
|
56
|
+
if (${jsonMode}) {
|
|
57
|
+
console.log(JSON.stringify(report, null, 2));
|
|
58
|
+
} else {
|
|
59
|
+
console.log('Status:', report.alignment_status);
|
|
60
|
+
console.log('Score:', report.score);
|
|
61
|
+
console.log('Misaligned:', report.misaligned_tests.length);
|
|
62
|
+
}
|
|
63
|
+
process.exit(report.alignment_status === 'PASS' ? 0 : 1);
|
|
64
|
+
"`, { stdio: 'inherit' });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
const COMMANDS = {
|
|
32
72
|
'init': {
|
|
33
73
|
description: 'Initialize xp-gate (use --global for all projects)',
|
|
@@ -131,6 +171,11 @@ const COMMANDS = {
|
|
|
131
171
|
run: subargs => arch(subargs).then(code => process.exit(code)),
|
|
132
172
|
usage: 'xp-gate arch [--config <path>]'
|
|
133
173
|
},
|
|
174
|
+
'check-alignment': {
|
|
175
|
+
description: 'Run test-specification alignment check (deterministic, no LLM)',
|
|
176
|
+
run: subargs => runAlignmentCheck(subargs).then(code => process.exit(code)),
|
|
177
|
+
usage: 'xp-gate check-alignment [--spec <path>] [--tests <dir>] [--json] [--output <path>]'
|
|
178
|
+
},
|
|
134
179
|
'upgrade': {
|
|
135
180
|
description: 'Check for xp-gate updates (--preview for JSON, --apply to upgrade)',
|
|
136
181
|
run: subargs => upgrade(subargs).then(code => process.exit(code)),
|
package/hooks/pre-commit
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# OpenCode Quality Gates - Pre-Commit Hook -
|
|
2
|
+
# OpenCode Quality Gates - Pre-Commit Hook - 12 numbered gates (Gate 0–11)
|
|
3
3
|
#
|
|
4
|
-
# DESIGN PRINCIPLE:
|
|
5
|
-
#
|
|
4
|
+
# DESIGN PRINCIPLE: Tool unavailable = SKIP (graceful degradation), tool available + check fails = BLOCK
|
|
5
|
+
# Missing tools do NOT block the commit — the gate degrades to SKIP. Block fires only when
|
|
6
|
+
# the tool exists AND the check fails. This prevents broken toolchains from blocking work.
|
|
6
7
|
#
|
|
7
|
-
# Converts 9 sequential gates into 6 type-based comprehensive gates
|
|
8
8
|
# Sourced from adapter-common.sh and language-specific adapters
|
|
9
9
|
|
|
10
10
|
# Source the common adapter functions — 3-tier resolution:
|
|
@@ -1328,7 +1328,9 @@ source "$GATE_DIR/gate-4.sh"
|
|
|
1328
1328
|
# v0.14.3+ (Issue #312): Skip test run when no code files changed.
|
|
1329
1329
|
# Code files = src/**/*.ts, plugins/**/*.ts, *.ts, *.tsx, etc. (language-aware).
|
|
1330
1330
|
# VERSION, CHANGELOG, docs, config-only changes → SKIP test run entirely.
|
|
1331
|
-
# Gate 5a (new file pairing)
|
|
1331
|
+
# Gate 5a (new file pairing), 5b (mock density), and 5c (semantic annotation)
|
|
1332
|
+
# are lightweight and still run even when no code files changed.
|
|
1333
|
+
# Gate 5a (new file pairing) and Gate 5b (mock density) still run — they're lightweight.
|
|
1332
1334
|
# ============================================================================
|
|
1333
1335
|
2>&1 echo ""
|
|
1334
1336
|
2>&1 echo "→ Gate 5: Tests & coverage..."
|
|
@@ -1640,6 +1642,82 @@ else
|
|
|
1640
1642
|
done
|
|
1641
1643
|
fi
|
|
1642
1644
|
|
|
1645
|
+
# ============================================================================
|
|
1646
|
+
# Gate 5c: Semantic Annotation Check (v0.19.0+)
|
|
1647
|
+
# Validates @test REQ-XXX annotations against specification.yaml.
|
|
1648
|
+
#
|
|
1649
|
+
# Strategy: Reads pre-compiled req-ids.json (produced by xp-gate check-alignment).
|
|
1650
|
+
# If req-ids.json is absent or stale, degrades to SKIP.
|
|
1651
|
+
# If present: greps staged tests for @test REQ-XXX, cross-references against
|
|
1652
|
+
# the pre-compiled REQ ID list. BLOCKs if any REQ ID doesn't exist in spec.
|
|
1653
|
+
# If any REQ in spec has no annotated test, emits visible [FAIL] message
|
|
1654
|
+
# (exit 0, appears in hook summary) — full alignment check runs at CLI time.
|
|
1655
|
+
# ============================================================================
|
|
1656
|
+
REQ_IDS_FILE=".sprint-state/phase-outputs/req-ids.json"
|
|
1657
|
+
if [ -f "$REQ_IDS_FILE" ] && command -v jq >/dev/null 2>&1; then
|
|
1658
|
+
# Check staleness: req-ids.json must be newer than specification.yaml
|
|
1659
|
+
SPEC_FILE="specification.yaml"
|
|
1660
|
+
REQ_IDS_STALE=false
|
|
1661
|
+
if [ -f "$SPEC_FILE" ]; then
|
|
1662
|
+
if [ "$REQ_IDS_FILE" -ot "$SPEC_FILE" ]; then
|
|
1663
|
+
REQ_IDS_STALE=true
|
|
1664
|
+
fi
|
|
1665
|
+
fi
|
|
1666
|
+
|
|
1667
|
+
if [ "$REQ_IDS_STALE" = true ]; then
|
|
1668
|
+
echo "⚠️ Gate 5c: req-ids.json is stale (specification.yaml updated since last check-alignment)"
|
|
1669
|
+
echo " Run 'xp-gate check-alignment' to regenerate before next commit."
|
|
1670
|
+
else
|
|
1671
|
+
CHANGED_TEST_FILES_ANNOTATED=$(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true)
|
|
1672
|
+
if [ -n "$CHANGED_TEST_FILES_ANNOTATED" ]; then
|
|
1673
|
+
echo " → Checking @test REQ-XXX annotations..."
|
|
1674
|
+
ANNOTATION_ERRORS=0
|
|
1675
|
+
for tf in $CHANGED_TEST_FILES_ANNOTATED; do
|
|
1676
|
+
if [ -f "$tf" ]; then
|
|
1677
|
+
# Extract @test REQ-XXX annotations from staged test file (word-bounded)
|
|
1678
|
+
ANNOTATED_REQS=$(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' | sort -u || true)
|
|
1679
|
+
for req_id in $ANNOTATED_REQS; do
|
|
1680
|
+
# Cross-reference against pre-compiled REQ ID list
|
|
1681
|
+
FOUND=$(jq -r --arg rid "$req_id" '.[] | select(. == $rid)' "$REQ_IDS_FILE" 2>/dev/null || true)
|
|
1682
|
+
if [ -z "$FOUND" ]; then
|
|
1683
|
+
echo "❌ Gate 5c: $tf — @test $req_id: REQ ID not found in specification.yaml"
|
|
1684
|
+
ANNOTATION_ERRORS=$((ANNOTATION_ERRORS + 1))
|
|
1685
|
+
fi
|
|
1686
|
+
done
|
|
1687
|
+
fi
|
|
1688
|
+
done
|
|
1689
|
+
|
|
1690
|
+
if [ "$ANNOTATION_ERRORS" -gt 0 ]; then
|
|
1691
|
+
echo ""
|
|
1692
|
+
echo "❌ $ANNOTATION_ERRORS test annotation(s) reference non-existent REQ IDs — commit BLOCKED"
|
|
1693
|
+
echo " Fix: Verify REQ IDs against specification.yaml, or run 'xp-gate check-alignment'"
|
|
1694
|
+
exit 1
|
|
1695
|
+
fi
|
|
1696
|
+
|
|
1697
|
+
# Check reverse: any REQ in spec with no annotated test → visible FAIL (non-blocking)
|
|
1698
|
+
ALL_SPEC_REQS=$(jq -r '.[]' "$REQ_IDS_FILE" 2>/dev/null)
|
|
1699
|
+
ANNOTATED_ALL=""
|
|
1700
|
+
for tf in $(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true); do
|
|
1701
|
+
if [ -f "$tf" ]; then
|
|
1702
|
+
ANNOTATED_ALL="$ANNOTATED_ALL $(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' || true)"
|
|
1703
|
+
fi
|
|
1704
|
+
done
|
|
1705
|
+
UNTESTED_COUNT=0
|
|
1706
|
+
for spec_req in $ALL_SPEC_REQS; do
|
|
1707
|
+
if ! echo "$ANNOTATED_ALL" | grep -qF "$spec_req"; then
|
|
1708
|
+
UNTESTED_COUNT=$((UNTESTED_COUNT + 1))
|
|
1709
|
+
fi
|
|
1710
|
+
done
|
|
1711
|
+
if [ "$UNTESTED_COUNT" -gt 0 ]; then
|
|
1712
|
+
echo "⚠️ [FAIL] Gate 5c: $UNTESTED_COUNT REQ(s) in specification.yaml have no annotated tests"
|
|
1713
|
+
echo " This is non-blocking. Run 'xp-gate check-alignment' for full alignment report."
|
|
1714
|
+
fi
|
|
1715
|
+
|
|
1716
|
+
echo " ✅ @test annotation check complete."
|
|
1717
|
+
fi
|
|
1718
|
+
fi
|
|
1719
|
+
fi
|
|
1720
|
+
|
|
1643
1721
|
# ============================================================================
|
|
1644
1722
|
# v0.14.3+ (Issue #312): Early skip when no code files changed.
|
|
1645
1723
|
# If the commit only touches VERSION, CHANGELOG, docs, config, .gitignore,
|
|
@@ -2439,13 +2517,13 @@ generate_quality_report() {
|
|
|
2439
2517
|
local TIMESTAMP
|
|
2440
2518
|
local BRANCH
|
|
2441
2519
|
local PASSED_COUNT=0
|
|
2442
|
-
local TOTAL_GATES=
|
|
2520
|
+
local TOTAL_GATES=12
|
|
2443
2521
|
|
|
2444
2522
|
COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
2445
2523
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
|
2446
2524
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2447
2525
|
|
|
2448
|
-
for gate in 1 2 3 4 5 6 7 8 9 10 11; do
|
|
2526
|
+
for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
2449
2527
|
local status_var="GATE_${gate}_STATUS"
|
|
2450
2528
|
if [ "${!status_var}" = "PASS" ]; then
|
|
2451
2529
|
PASSED_COUNT=$((PASSED_COUNT + 1))
|
|
@@ -2558,6 +2636,11 @@ generate_quality_report() {
|
|
|
2558
2636
|
"name": "Sprint Flow Enforcement",
|
|
2559
2637
|
"status": "${GATE_11_STATUS:-PASS}",
|
|
2560
2638
|
"tool": "sprint-gate.sh"
|
|
2639
|
+
},
|
|
2640
|
+
"gate12_file_hygiene": {
|
|
2641
|
+
"name": "File Hygiene Check",
|
|
2642
|
+
"status": "${GATE_12_STATUS:-WARN}",
|
|
2643
|
+
"tool": "gate-12-file-hygiene.sh"
|
|
2561
2644
|
}
|
|
2562
2645
|
}
|
|
2563
2646
|
}
|
|
@@ -2583,8 +2666,8 @@ ENDJSON
|
|
|
2583
2666
|
GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
|
|
2584
2667
|
GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"Build Integrity\"},"
|
|
2585
2668
|
GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-PASS}\",\"name\":\"SAST Security\"},"
|
|
2586
|
-
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"}
|
|
2587
|
-
GATES_JSON="${GATES_JSON}}"
|
|
2669
|
+
GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
|
|
2670
|
+
GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"
|
|
2588
2671
|
|
|
2589
2672
|
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"
|
|
2590
2673
|
|
|
@@ -2605,6 +2688,7 @@ ENDJSON
|
|
|
2605
2688
|
printf " %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-PASS}"
|
|
2606
2689
|
printf " %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-PASS}"
|
|
2607
2690
|
printf " %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
|
|
2691
|
+
printf " %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
|
|
2608
2692
|
echo ""
|
|
2609
2693
|
echo " Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
|
|
2610
2694
|
echo " Branch status: $REPORT_FILE"
|
|
@@ -2612,8 +2696,23 @@ ENDJSON
|
|
|
2612
2696
|
echo " History saved: $HISTORY_FILE"
|
|
2613
2697
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
2614
2698
|
echo ""
|
|
2699
|
+
# AI-friendly common footer: tells LLM agents how to fix issues instead of bypassing
|
|
2700
|
+
echo " ── AI AGENT DIRECTIVE ──"
|
|
2701
|
+
echo " If any gate FAILED above:"
|
|
2702
|
+
echo " - Each error includes a file path and rule identifier"
|
|
2703
|
+
echo " - Fix each issue, then re-run: git commit"
|
|
2704
|
+
echo " - Do NOT use --no-verify — bypassed commits block sprint progression"
|
|
2705
|
+
echo " ─────────────────────────"
|
|
2706
|
+
echo ""
|
|
2615
2707
|
}
|
|
2616
2708
|
|
|
2709
|
+
# Post-commit bypass marker: write a sentinel file in .git/ directory
|
|
2710
|
+
# that the post-commit hook checks to detect --no-verify bypasses
|
|
2711
|
+
_wp_marker="$(git rev-parse --git-dir 2>/dev/null)/xp-gate-precommit-marker"
|
|
2712
|
+
if [ -n "$_wp_marker" ] && [ "$_wp_marker" != "/xp-gate-precommit-marker" ]; then
|
|
2713
|
+
echo "$(date +%s)" > "$_wp_marker"
|
|
2714
|
+
fi
|
|
2715
|
+
|
|
2617
2716
|
generate_quality_report
|
|
2618
2717
|
|
|
2619
2718
|
exit 0
|