@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.
Files changed (35) hide show
  1. package/adapters/gate-3.sh +68 -0
  2. package/adapters/gate-4.sh +80 -0
  3. package/adapters/gate-7.sh +45 -0
  4. package/adapters/gate-8.sh +56 -0
  5. package/adapters/gate-9.sh +105 -0
  6. package/bin/xp-gate.js +45 -0
  7. package/hooks/pre-commit +108 -9
  8. package/hooks/pre-push +63 -232
  9. package/lib/__tests__/install-skill.test.js +3 -3
  10. package/lib/__tests__/test-alignment.test.ts +761 -0
  11. package/lib/install-skill.js +11 -3
  12. package/lib/mattpocock-license-verdict.json +1 -1
  13. package/lib/phase-transition.js +106 -1
  14. package/lib/sprint-init.js +41 -0
  15. package/lib/test-alignment.ts +651 -0
  16. package/mock-policy/AGENTS.md +3 -3
  17. package/mutation/AGENTS.md +3 -3
  18. package/mutation/gate-m.ts +18 -4
  19. package/package.json +1 -1
  20. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  22. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  23. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  24. package/plugins/opencode/package.json +1 -1
  25. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  26. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  27. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  28. package/plugins/qoder/plugin.json +1 -1
  29. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  30. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  31. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  32. package/principles/AGENTS.md +3 -3
  33. package/skills/delphi-review/AGENTS.md +3 -3
  34. package/skills/sprint-flow/AGENTS.md +3 -3
  35. 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 - Refactored 6-Type-Based Gates
2
+ # OpenCode Quality Gates - Pre-Commit Hook - 12 numbered gates (Gate 0–11)
3
3
  #
4
- # DESIGN PRINCIPLE: Zero tolerance - tool unavailable = BLOCK (per CODE-OF-CONDUCT.md)
5
- # Each gate requires its tools to be installed; missing tools block the commit.
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) and Gate 5b (mock density) still run — they're lightweight.
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=11
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