@boyingliu01/xp-gate 0.15.3 → 0.15.5
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 +104 -0
- package/bin/xp-gate.js +1 -1
- package/hooks/pre-commit +9 -0
- package/lib/__tests__/install-cmd.test.js +0 -35
- package/lib/doctor-tui.js +198 -0
- package/lib/doctor.js +95 -179
- package/lib/init.js +6 -4
- package/lib/install-cmd.js +31 -42
- package/lib/uninstall.js +22 -0
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +10 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +10 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/sprint-flow/references/phase-4-verify.md +10 -0
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -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,104 @@
|
|
|
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"
|
package/bin/xp-gate.js
CHANGED
|
@@ -179,7 +179,7 @@ const COMMANDS = {
|
|
|
179
179
|
usage: 'xp-gate update-hooks [--global] [--force] [--dry-run] [--no-backup] [--scope hooks|adapters|all]'
|
|
180
180
|
},
|
|
181
181
|
'install': {
|
|
182
|
-
description: 'One-step install (init + bootstrap + doctor)',
|
|
182
|
+
description: 'One-step install (init + baseline + bootstrap + doctor --fix)',
|
|
183
183
|
run: subargs => {
|
|
184
184
|
const { install } = require('../lib/install-cmd');
|
|
185
185
|
install(subargs).then(code => process.exit(code));
|
package/hooks/pre-commit
CHANGED
|
@@ -174,6 +174,8 @@ echo ""
|
|
|
174
174
|
# ============================================================================
|
|
175
175
|
# Gate 0: Version Consistency Check (Protected Branches)
|
|
176
176
|
# ============================================================================
|
|
177
|
+
GATE_0_START=$(gate_start_ms)
|
|
178
|
+
GATE_0_STATUS="PASS"
|
|
177
179
|
|
|
178
180
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
179
181
|
PROTECTED_BRANCHES="master develop trunk mainline"
|
|
@@ -327,6 +329,8 @@ Or use --no-verify (discouraged) or include [skip-version-check] in commit messa
|
|
|
327
329
|
|
|
328
330
|
Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
|
|
329
331
|
"
|
|
332
|
+
GATE_0_STATUS="BLOCK"
|
|
333
|
+
record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "1" "$GATE_0_START"
|
|
330
334
|
exit 1
|
|
331
335
|
fi
|
|
332
336
|
fi
|
|
@@ -336,6 +340,8 @@ else
|
|
|
336
340
|
echo "✅ PASSED - Gate 0: Version Consistency Check (non-protected branch: $CURRENT_BRANCH)"
|
|
337
341
|
fi
|
|
338
342
|
|
|
343
|
+
record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "0" "$GATE_0_START"
|
|
344
|
+
|
|
339
345
|
echo ""
|
|
340
346
|
|
|
341
347
|
# Get list of changed files
|
|
@@ -2345,6 +2351,7 @@ fi
|
|
|
2345
2351
|
GATE_9_END=$(date +%s)
|
|
2346
2352
|
GATE_9_DURATION=$((GATE_9_END - GATE_9_START))
|
|
2347
2353
|
echo "✅ Gate 9 completed in ${GATE_9_DURATION}s"
|
|
2354
|
+
record_gate_audit "gate-9" "build-integrity" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
2348
2355
|
echo ""
|
|
2349
2356
|
# ============================================================================
|
|
2350
2357
|
|
|
@@ -2379,6 +2386,7 @@ if [ -n "$SPRINT_GATE_SCRIPT" ]; then
|
|
|
2379
2386
|
if ! bash "$SPRINT_GATE_SCRIPT" --pre-commit; then
|
|
2380
2387
|
GATE_11_STATUS="BLOCK"
|
|
2381
2388
|
echo "❌ BLOCKED - Gate 11: Sprint Flow Enforcement"
|
|
2389
|
+
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "1" "$GATE_11_START"
|
|
2382
2390
|
# Sprint gate failure is a hard block — exit immediately
|
|
2383
2391
|
exit 1
|
|
2384
2392
|
fi
|
|
@@ -2390,6 +2398,7 @@ fi
|
|
|
2390
2398
|
GATE_11_END=$(date +%s)
|
|
2391
2399
|
GATE_11_DURATION=$((GATE_11_END - GATE_11_START))
|
|
2392
2400
|
echo "✅ Gate 11 completed in ${GATE_11_DURATION}s"
|
|
2401
|
+
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "0" "$GATE_11_START"
|
|
2393
2402
|
echo ""
|
|
2394
2403
|
# ============================================================================
|
|
2395
2404
|
|
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
1
|
describe('install-cmd', () => {
|
|
6
|
-
let tmpDir;
|
|
7
|
-
let consoleLogSpy;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-install-'));
|
|
11
|
-
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
12
|
-
vi.resetModules();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
-
vi.restoreAllMocks();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
2
|
it('exports install function', () => {
|
|
21
3
|
const { install } = require('../install-cmd');
|
|
22
4
|
expect(typeof install).toBe('function');
|
|
23
5
|
});
|
|
24
|
-
|
|
25
|
-
it('install accepts --global flag', async () => {
|
|
26
|
-
const { install } = require('../install-cmd');
|
|
27
|
-
const code = await install(['--global'], tmpDir);
|
|
28
|
-
expect(typeof code).toBe('number');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('install runs without --global (local mode) in git repo', async () => {
|
|
32
|
-
// Simulate git repo
|
|
33
|
-
const gitDir = path.join(tmpDir, '.git');
|
|
34
|
-
fs.mkdirSync(gitDir, { recursive: true });
|
|
35
|
-
fs.mkdirSync(path.join(gitDir, 'hooks'), { recursive: true });
|
|
36
|
-
|
|
37
|
-
const { install } = require('../install-cmd');
|
|
38
|
-
const code = await install([], tmpDir);
|
|
39
|
-
expect(typeof code).toBe('number');
|
|
40
|
-
});
|
|
41
6
|
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI registration and installed-skills diagnostics for doctor.
|
|
3
|
+
* Extracted from doctor.js to keep file size under the large-file threshold.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { HOME_DIR, detectPlatform } = require('./shared-paths.js');
|
|
8
|
+
|
|
9
|
+
// npm package source dir (template hooks/adapters)
|
|
10
|
+
const PKG_DIR = path.dirname(__dirname);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TUI registration path and expected plugin entry.
|
|
14
|
+
*/
|
|
15
|
+
const TUI_JSON_PATH = path.join(HOME_DIR, '.config', 'opencode', 'tui.json');
|
|
16
|
+
const TUI_PLUGIN_ENTRY = '@boyingliu01/opencode-plugin/tui';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read and parse tui.json, returning { data, error }.
|
|
20
|
+
* data = parsed object on success, null on file missing, undefined on corrupt.
|
|
21
|
+
*/
|
|
22
|
+
function readTuiJson() {
|
|
23
|
+
if (!fs.existsSync(TUI_JSON_PATH)) return { data: null, error: null };
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(TUI_JSON_PATH, 'utf8');
|
|
26
|
+
const data = JSON.parse(raw);
|
|
27
|
+
return { data, error: null };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return { data: undefined, error: `Corrupt JSON: ${e.message}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check 9: TUI auto-registration in ~/.config/opencode/tui.json.
|
|
35
|
+
* @returns {number} issue count
|
|
36
|
+
*/
|
|
37
|
+
function diagnoseTuiRegistration(checks) {
|
|
38
|
+
const { data, error } = readTuiJson();
|
|
39
|
+
|
|
40
|
+
if (error) {
|
|
41
|
+
checks.push({ name: 'TUI registration', status: 'FAIL', detail: error });
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (data === null) {
|
|
46
|
+
checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const plugins = Array.isArray(data.plugin) ? data.plugin : [];
|
|
51
|
+
if (plugins.includes(TUI_PLUGIN_ENTRY)) {
|
|
52
|
+
checks.push({ name: 'TUI registration', status: 'PASS', detail: `${TUI_PLUGIN_ENTRY} registered` });
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check 9: Installed skills vs package-bundled skills (#332).
|
|
62
|
+
* Reports WARN for each installed skill whose SKILL.md differs from the bundled version.
|
|
63
|
+
* @param {object} config
|
|
64
|
+
* @param {Array} checks
|
|
65
|
+
* @returns {number} issue count
|
|
66
|
+
*/
|
|
67
|
+
function diagnoseInstalledSkills(config, checks) {
|
|
68
|
+
const installedSkills = config.installedSkills || {};
|
|
69
|
+
const skillNames = Object.keys(installedSkills);
|
|
70
|
+
if (skillNames.length === 0) {
|
|
71
|
+
checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'No skills installed' });
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const bundledSkillsDir = path.join(PKG_DIR, 'skills');
|
|
76
|
+
if (!fs.existsSync(bundledSkillsDir)) {
|
|
77
|
+
checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'Package skills dir not found' });
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const platform = detectPlatform();
|
|
82
|
+
let userSkillsDir;
|
|
83
|
+
if (platform === 'qoder') {
|
|
84
|
+
userSkillsDir = path.join(HOME_DIR, '.qoder', 'skills');
|
|
85
|
+
} else if (platform === 'claude-code') {
|
|
86
|
+
userSkillsDir = path.join(HOME_DIR, '.claude', 'skills');
|
|
87
|
+
} else {
|
|
88
|
+
userSkillsDir = path.join(HOME_DIR, '.config', 'opencode', 'skills');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let issues = 0;
|
|
92
|
+
for (const name of skillNames) {
|
|
93
|
+
const bundledSkillMd = path.join(bundledSkillsDir, name, 'SKILL.md');
|
|
94
|
+
const userSkillMd = path.join(userSkillsDir, name, 'SKILL.md');
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(bundledSkillMd)) continue;
|
|
97
|
+
if (!fs.existsSync(userSkillMd)) {
|
|
98
|
+
checks.push({ name: `Skill: ${name}`, status: 'FAIL', detail: 'Installed SKILL.md missing' });
|
|
99
|
+
issues++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const bundledContent = fs.readFileSync(bundledSkillMd, 'utf8');
|
|
104
|
+
const userContent = fs.readFileSync(userSkillMd, 'utf8');
|
|
105
|
+
if (bundledContent !== userContent) {
|
|
106
|
+
checks.push({
|
|
107
|
+
name: `Skill: ${name}`,
|
|
108
|
+
status: 'WARN',
|
|
109
|
+
detail: 'Outdated — run xp-gate update-skill --all',
|
|
110
|
+
});
|
|
111
|
+
issues++;
|
|
112
|
+
} else {
|
|
113
|
+
checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return issues;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fix TUI registration: ensure @boyingliu01/opencode-plugin/tui is in tui.json.
|
|
122
|
+
* Uses atomic write (tmp + renameSync) for JSON safety.
|
|
123
|
+
* On corrupt JSON: backup to .corrupt-{timestamp}.bak then rebuild.
|
|
124
|
+
* Idempotent: skips if already registered.
|
|
125
|
+
* @returns {boolean} Whether a fix was applied
|
|
126
|
+
*/
|
|
127
|
+
function fixTuiRegistration() {
|
|
128
|
+
const { data, error } = readTuiJson();
|
|
129
|
+
|
|
130
|
+
// Corrupt JSON: backup old file, rebuild from scratch
|
|
131
|
+
if (error && data === undefined) {
|
|
132
|
+
const ts = Date.now();
|
|
133
|
+
const backupPath = `${TUI_JSON_PATH}.corrupt-${ts}.bak`;
|
|
134
|
+
try {
|
|
135
|
+
fs.copyFileSync(TUI_JSON_PATH, backupPath);
|
|
136
|
+
console.log(` ⚠ TUI config corrupted — backed up to ${backupPath}`);
|
|
137
|
+
} catch { /* non-critical */ }
|
|
138
|
+
// Rebuild fresh
|
|
139
|
+
const dir = path.dirname(TUI_JSON_PATH);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
|
|
142
|
+
const tmpPath = `${TUI_JSON_PATH}.tmp`;
|
|
143
|
+
fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
|
|
144
|
+
fs.renameSync(tmpPath, TUI_JSON_PATH);
|
|
145
|
+
console.log(` ✓ Registered ${TUI_PLUGIN_ENTRY} in TUI (rebuilt after corrupt backup)`);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// File doesn't exist: create it
|
|
150
|
+
if (data === null) {
|
|
151
|
+
const dir = path.dirname(TUI_JSON_PATH);
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
|
|
154
|
+
const tmpPath = `${TUI_JSON_PATH}.tmp`;
|
|
155
|
+
fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
|
|
156
|
+
fs.renameSync(tmpPath, TUI_JSON_PATH);
|
|
157
|
+
console.log(` ✓ Created TUI config with ${TUI_PLUGIN_ENTRY}`);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// File exists, check if plugin already registered (idempotent)
|
|
162
|
+
const plugins = Array.isArray(data.plugin) ? data.plugin : [];
|
|
163
|
+
if (plugins.includes(TUI_PLUGIN_ENTRY)) return false;
|
|
164
|
+
|
|
165
|
+
// Append plugin entry
|
|
166
|
+
data.plugin = plugins.concat([TUI_PLUGIN_ENTRY]);
|
|
167
|
+
const tmpPath = `${TUI_JSON_PATH}.tmp`;
|
|
168
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
169
|
+
fs.renameSync(tmpPath, TUI_JSON_PATH);
|
|
170
|
+
console.log(` ✓ Added ${TUI_PLUGIN_ENTRY} to TUI config`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wrapper for init.js: ensure TUI is registered without console output.
|
|
176
|
+
* Returns true if the plugin is already registered or was just added.
|
|
177
|
+
*/
|
|
178
|
+
function ensureTuiRegistration() {
|
|
179
|
+
const { data } = readTuiJson();
|
|
180
|
+
if (data === null || data === undefined) {
|
|
181
|
+
fixTuiRegistration();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const plugins = Array.isArray(data.plugin) ? data.plugin : [];
|
|
185
|
+
if (!plugins.includes(TUI_PLUGIN_ENTRY)) {
|
|
186
|
+
fixTuiRegistration();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
TUI_JSON_PATH,
|
|
192
|
+
TUI_PLUGIN_ENTRY,
|
|
193
|
+
readTuiJson,
|
|
194
|
+
diagnoseTuiRegistration,
|
|
195
|
+
diagnoseInstalledSkills,
|
|
196
|
+
fixTuiRegistration,
|
|
197
|
+
ensureTuiRegistration,
|
|
198
|
+
};
|