@boyingliu01/xp-gate 0.14.30 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/adapters/java.sh +33 -4
  2. package/gate-9.sh +22 -48
  3. package/hooks/gate-9.sh +22 -48
  4. package/hooks/pre-commit +102 -44
  5. package/hooks/pre-push +1 -33
  6. package/lib/__tests__/doctor.test.js +34 -31
  7. package/lib/detect-deps.js +12 -0
  8. package/mock-policy/AGENTS.md +3 -3
  9. package/mutation/AGENTS.md +3 -3
  10. package/package.json +2 -2
  11. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  12. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  13. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  14. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -0
  15. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +71 -0
  16. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  17. package/plugins/opencode/package.json +1 -1
  18. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  19. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  20. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -0
  21. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +71 -0
  22. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/qoder/plugin.json +1 -1
  24. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  25. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  26. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  27. package/principles/AGENTS.md +3 -3
  28. package/principles/adapters/python.ts +39 -6
  29. package/scripts/__tests__/sync-package-content.test.js +36 -17
  30. package/scripts/sync-package-content.js +5 -4
  31. package/skills/delphi-review/AGENTS.md +3 -3
  32. package/skills/sprint-flow/AGENTS.md +3 -3
  33. package/skills/sprint-flow/SKILL.md +1 -0
  34. package/skills/sprint-flow/references/phase-3-build.md +71 -0
  35. package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/java.sh CHANGED
@@ -79,16 +79,45 @@ run_tests() {
79
79
  fi
80
80
  }
81
81
 
82
+ _detect_jacoco_configured() {
83
+ local build_system="$1"
84
+ if [ "$build_system" = "maven" ]; then
85
+ # Check for jacoco-maven-plugin in pom.xml
86
+ grep -q 'jacoco-maven-plugin\|org.jacoco' pom.xml 2>/dev/null && return 0
87
+ # Check for jacoco.xml report already generated
88
+ [ -f "target/site/jacoco/jacoco.xml" ] && return 0
89
+ return 1
90
+ elif [ "$build_system" = "gradle" ]; then
91
+ # Check for jacoco plugin in build.gradle or build.gradle.kts
92
+ grep -q "id.*jacoco\|apply.*plugin.*jacoco\|jacoco" build.gradle build.gradle.kts 2>/dev/null && return 0
93
+ [ -f "build/reports/jacoco/test/jacocoTestReport.xml" ] && return 0
94
+ return 1
95
+ fi
96
+ return 1
97
+ }
98
+
82
99
  run_coverage() {
83
100
  local build_system
84
101
  build_system=$(_detect_java_build)
85
102
 
86
103
  if [ "$build_system" = "maven" ]; then
87
- mvn test jacoco:report -q 2>&1 | tail -10
88
- return "${PIPESTATUS[0]}"
104
+ if _detect_jacoco_configured "$build_system"; then
105
+ mvn test jacoco:report -q 2>&1 | tail -10
106
+ return "${PIPESTATUS[0]}"
107
+ else
108
+ echo "ℹ️ JaCoCo not configured in pom.xml — SKIP coverage check"
109
+ echo " To enable: add jacoco-maven-plugin to your pom.xml"
110
+ return 0
111
+ fi
89
112
  elif [ "$build_system" = "gradle" ]; then
90
- gradle jacocoTestReport --quiet 2>&1 | tail -10
91
- return "${PIPESTATUS[0]}"
113
+ if _detect_jacoco_configured "$build_system"; then
114
+ gradle jacocoTestReport --quiet 2>&1 | tail -10
115
+ return "${PIPESTATUS[0]}"
116
+ else
117
+ echo "ℹ️ JaCoCo not configured in Gradle build — SKIP coverage check"
118
+ echo " To enable: add 'id \"jacoco\"' to your build.gradle plugins block"
119
+ return 0
120
+ fi
92
121
  else
93
122
  echo "No Maven/Gradle project detected"
94
123
  return 1
package/gate-9.sh CHANGED
@@ -44,57 +44,31 @@ else
44
44
  GATE_9_STATUS="PASS"
45
45
  elif [ "$SEMGREP_EXIT" -eq 1 ]; then
46
46
  # Findings detected - parse JSON to categorize
47
- CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
48
- import sys, json
49
- try:
50
- data = json.load(sys.stdin)
51
- results = data.get('results', [])
52
- count = 0
53
- for r in results:
54
- extra = r.get('extra', {})
55
- severity = extra.get('severity', '').upper()
56
- if severity in ('CRITICAL', 'HIGH'):
57
- count += 1
58
- print(count)
59
- except:
60
- print('0')
61
- " 2>/dev/null || echo "0")
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")
62
53
 
63
- MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | python3 -c "
64
- import sys, json
65
- try:
66
- data = json.load(sys.stdin)
67
- results = data.get('results', [])
68
- count = 0
69
- for r in results:
70
- extra = r.get('extra', {})
71
- severity = extra.get('severity', '').upper()
72
- if severity in ('MEDIUM', 'LOW'):
73
- count += 1
74
- print(count)
75
- except:
76
- print('0')
77
- " 2>/dev/null || echo "0")
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")
78
60
 
79
61
  # Extract top finding details
80
- FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | python3 -c "
81
- import sys, json
82
- try:
83
- data = json.load(sys.stdin)
84
- results = data.get('results', [])
85
- for r in results[:5]: # Show top 5
86
- extra = r.get('extra', {})
87
- severity = extra.get('severity', 'UNKNOWN').upper()
88
- rule_id = r.get('check_id', 'unknown')
89
- path = r.get('path', 'unknown')
90
- start_line = r.get('start', {}).get('line', '?')
91
- message = extra.get('message', '')[:80]
92
- print(f' [{severity}] {rule_id}')
93
- print(f' {path}:{start_line} → {message}')
94
- print()
95
- except:
96
- print(' (Failed to parse semgrep output)')
97
- " 2>/dev/null || echo " (Failed to parse semgrep output)")
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)")
98
72
 
99
73
  if [ "$CRITICAL_HIGH" -gt 0 ]; then
100
74
  echo ""
package/hooks/gate-9.sh CHANGED
@@ -44,57 +44,31 @@ else
44
44
  GATE_9_STATUS="PASS"
45
45
  elif [ "$SEMGREP_EXIT" -eq 1 ]; then
46
46
  # Findings detected - parse JSON to categorize
47
- CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
48
- import sys, json
49
- try:
50
- data = json.load(sys.stdin)
51
- results = data.get('results', [])
52
- count = 0
53
- for r in results:
54
- extra = r.get('extra', {})
55
- severity = extra.get('severity', '').upper()
56
- if severity in ('CRITICAL', 'HIGH'):
57
- count += 1
58
- print(count)
59
- except:
60
- print('0')
61
- " 2>/dev/null || echo "0")
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")
62
53
 
63
- MEDIUM_LOW=$(echo "$SEMGREP_OUTPUT" | python3 -c "
64
- import sys, json
65
- try:
66
- data = json.load(sys.stdin)
67
- results = data.get('results', [])
68
- count = 0
69
- for r in results:
70
- extra = r.get('extra', {})
71
- severity = extra.get('severity', '').upper()
72
- if severity in ('MEDIUM', 'LOW'):
73
- count += 1
74
- print(count)
75
- except:
76
- print('0')
77
- " 2>/dev/null || echo "0")
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")
78
60
 
79
61
  # Extract top finding details
80
- FINDING_DETAILS=$(echo "$SEMGREP_OUTPUT" | python3 -c "
81
- import sys, json
82
- try:
83
- data = json.load(sys.stdin)
84
- results = data.get('results', [])
85
- for r in results[:5]: # Show top 5
86
- extra = r.get('extra', {})
87
- severity = extra.get('severity', 'UNKNOWN').upper()
88
- rule_id = r.get('check_id', 'unknown')
89
- path = r.get('path', 'unknown')
90
- start_line = r.get('start', {}).get('line', '?')
91
- message = extra.get('message', '')[:80]
92
- print(f' [{severity}] {rule_id}')
93
- print(f' {path}:{start_line} → {message}')
94
- print()
95
- except:
96
- print(' (Failed to parse semgrep output)')
97
- " 2>/dev/null || echo " (Failed to parse semgrep output)")
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)")
98
72
 
99
73
  if [ "$CRITICAL_HIGH" -gt 0 ]; then
100
74
  echo ""
package/hooks/pre-commit CHANGED
@@ -212,13 +212,20 @@ if [ "$is_protected" = "true" ]; then
212
212
 
213
213
  # If either VERSION or CHANGELOG.md is staged, pass
214
214
  if [ "$has_version_change" = "true" ] || [ "$has_changelog_change" = "true" ]; then
215
- # Auto-run sync-version.sh when VERSION is staged (issue #206 follow-up).
216
- # Propagates VERSION → 4 package.json + refreshes 32 AGENTS.md headers,
217
- # then stages only files sync-version.sh is known to touch (NOT runtime
215
+ # Auto-run sync-version when VERSION is staged (issue #206 follow-up).
216
+ # Propagates VERSION → 4 package.json + refreshes AGENTS.md headers,
217
+ # then stages only files sync-version is known to touch (NOT runtime
218
218
  # journals or unrelated edits) so the commit captures it atomically.
219
- if [ "$has_version_change" = "true" ] && [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
220
- echo "🔄 Gate 0: VERSION staged — running sync-version.sh to fan out to package.json + AGENTS.md headers..."
221
- if bash "$ROOT_DIR/scripts/sync-version.sh" > /tmp/xp-gate-sync-version.log 2>&1; then
219
+ # Prefer Node.js (.cjs) for cross-platform; fall back to bash (.sh).
220
+ SYNC_VERSION_CMD=""
221
+ if [ -f "$ROOT_DIR/scripts/sync-version.cjs" ] && command -v node >/dev/null 2>&1; then
222
+ SYNC_VERSION_CMD="node $ROOT_DIR/scripts/sync-version.cjs"
223
+ elif [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
224
+ SYNC_VERSION_CMD="bash $ROOT_DIR/scripts/sync-version.sh"
225
+ fi
226
+ if [ "$has_version_change" = "true" ] && [ -n "$SYNC_VERSION_CMD" ]; then
227
+ echo "🔄 Gate 0: VERSION staged — running sync-version to fan out to package.json + AGENTS.md headers..."
228
+ if $SYNC_VERSION_CMD > /tmp/xp-gate-sync-version.log 2>&1; then
222
229
  SYNC_STAGED=0
223
230
  # Stage the 4 known package.json targets if modified
224
231
  for pkg in package.json src/npm-package/package.json \
@@ -238,9 +245,9 @@ if [ "$is_protected" = "true" ]; then
238
245
  SYNC_STAGED=$((SYNC_STAGED + 1))
239
246
  fi
240
247
  done < <(find "$ROOT_DIR" -name 'AGENTS.md' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0)
241
- echo " auto-staged $SYNC_STAGED file(s) from sync-version.sh (package.json × ≤6 + AGENTS.md headers)"
248
+ echo " auto-staged $SYNC_STAGED file(s) from sync-version (package.json × ≤6 + AGENTS.md headers)"
242
249
  else
243
- echo "⚠️ sync-version.sh failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
250
+ echo "⚠️ sync-version failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
244
251
  fi
245
252
  fi
246
253
  echo "✅ PASSED - Gate 0: Version Consistency Check (VERSION/CHANGELOG staged)"
@@ -314,7 +321,7 @@ This bypasses the ship workflow and causes version drift.
314
321
  Before committing:
315
322
  1. Update VERSION file with new version
316
323
  2. Add entry to CHANGELOG.md
317
- 3. Run: bash scripts/sync-version.sh
324
+ 3. Run: node scripts/sync-version.cjs
318
325
 
319
326
  Or use --no-verify (discouraged) or include [skip-version-check] in commit message.
320
327
 
@@ -347,13 +354,25 @@ fi
347
354
  # v0.15.1+; ARCH-03 fix), PROJECT_SUBDIRS (per-language subdir override, empty = root).
348
355
  # ============================================================================
349
356
 
350
- # XP_GATE_LANG env var override — skip all detection, use single language
351
- if [ -n "${XP_GATE_LANG:-}" ]; then
352
- PROJECT_LANGS="$XP_GATE_LANG"
353
- PROJECT_LANG="$XP_GATE_LANG"
357
+ # Language override resolution (project-level > git config > env var)
358
+ # Priority: .xp-gate-lang file > git config xp-gate.lang > XP_GATE_LANG env
359
+ _RESOLVED_LANG_OVERRIDE=""
360
+ if [ -f ".xp-gate-lang" ]; then
361
+ _RESOLVED_LANG_OVERRIDE=$(sed -n '1p' .xp-gate-lang 2>/dev/null | tr -d '[:space:]')
362
+ elif git config xp-gate.lang >/dev/null 2>&1; then
363
+ _RESOLVED_LANG_OVERRIDE=$(git config xp-gate.lang 2>/dev/null)
364
+ elif [ -n "${XP_GATE_LANG:-}" ]; then
365
+ _RESOLVED_LANG_OVERRIDE="$XP_GATE_LANG"
366
+ echo "⚠️ XP_GATE_LANG env var is deprecated for multi-project use."
367
+ echo " Prefer: echo '$XP_GATE_LANG' > .xp-gate-lang OR git config xp-gate.lang $XP_GATE_LANG"
368
+ fi
369
+
370
+ if [ -n "$_RESOLVED_LANG_OVERRIDE" ]; then
371
+ PROJECT_LANGS="$_RESOLVED_LANG_OVERRIDE"
372
+ PROJECT_LANG="$_RESOLVED_LANG_OVERRIDE"
354
373
  echo ""
355
374
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
356
- echo " 🔧 XP_GATE_LANG OVERRIDE: $PROJECT_LANG"
375
+ echo " 🔧 LANGUAGE OVERRIDE: $PROJECT_LANG"
357
376
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
358
377
  echo ""
359
378
  else
@@ -682,7 +701,7 @@ record_gate_audit() {
682
701
  local issues_found="$4"
683
702
  local start_ms="$5"
684
703
  local end_ms
685
- end_ms=$(date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0")
704
+ end_ms=$(date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0")
686
705
  local duration_ms=$((end_ms - start_ms))
687
706
  if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
688
707
  duration_ms=0
@@ -710,7 +729,7 @@ record_gate_audit() {
710
729
 
711
730
  # Get milliseconds timestamp (used by gates for duration measurement)
712
731
  gate_start_ms() {
713
- date +%s%3N 2>/dev/null || python3 -c "import time;print(int(time.time()*1000))" 2>/dev/null || echo "0"
732
+ date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0"
714
733
  }
715
734
 
716
735
  # Switch to subdirectory if detected (for single-language subdirectory projects).
@@ -825,13 +844,13 @@ else
825
844
  # Compare against lint baseline
826
845
  BASELINE_WARNINGS=$(echo "$ESLINT_FILES" | tr ' ' '\n' | node -e "
827
846
  const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
828
- const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
847
+ const stdin=require('fs').readFileSync(0,'utf8').trim();
829
848
  const files=stdin?stdin.split('\n'):[];
830
849
  let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
831
850
  console.log(total);
832
851
  ")
833
852
  CURRENT_WARNINGS=$(echo "$ESLINT_OUTPUT" | node -e "
834
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
853
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
835
854
  console.log(d.reduce((s,f)=>s+(f.warningCount||0)+(f.errorCount||0),0));
836
855
  ")
837
856
  if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
@@ -900,6 +919,20 @@ else
900
919
  fi
901
920
  fi
902
921
  fi
922
+
923
+ # Debug statement detection (debugger statement in JS/TS)
924
+ TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
925
+ if [ -n "$TS_DEBUG_FILES" ]; then
926
+ DEBUGGER_HITS=$(grep -n '^\s*debugger\s*;\?\s*$' $TS_DEBUG_FILES 2>/dev/null || true)
927
+ if [ -n "$DEBUGGER_HITS" ]; then
928
+ echo ""
929
+ echo "❌ BLOCKED - 'debugger' statements detected:"
930
+ echo "$DEBUGGER_HITS" | head -10
931
+ echo ""
932
+ echo "Remove debugger statements before committing."
933
+ exit 1
934
+ fi
935
+ fi
903
936
  fi
904
937
  ;;
905
938
 
@@ -920,19 +953,19 @@ else
920
953
  if [ -n "$PY_FILES" ]; then
921
954
  BASELINE_WARNINGS=$(echo "$PY_FILES" | node -e "
922
955
  const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
923
- const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
956
+ const stdin=require('fs').readFileSync(0,'utf8').trim();
924
957
  const files=stdin?stdin.split('\n'):[];
925
958
  let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
926
959
  console.log(total);
927
960
  ")
928
961
  CURRENT_WARNINGS=$(echo "$RUFF_OUTPUT" | node -e "
929
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
962
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
930
963
  console.log(d.reduce((s,f)=>s+(f.messages?f.messages.length:0),0));
931
964
  ")
932
965
  if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
933
966
  NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
934
967
  echo "$RUFF_OUTPUT" | node -e "
935
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
968
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
936
969
  for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
937
970
  " | head -30
938
971
  echo ""
@@ -952,7 +985,7 @@ else
952
985
  fi
953
986
  else
954
987
  echo "$RUFF_OUTPUT" | node -e "
955
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
988
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
956
989
  for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
957
990
  " | head -30
958
991
  echo ""
@@ -965,6 +998,37 @@ else
965
998
  else
966
999
  echo "✅ PASSED - Ruff linting."
967
1000
  fi
1001
+
1002
+ # Ruff format check (formatting gate)
1003
+ if command -v ruff &> /dev/null; then
1004
+ PY_FMT_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
1005
+ if [ -n "$PY_FMT_FILES" ]; then
1006
+ RUFF_FMT_OUTPUT=$(ruff format --check $PY_FMT_FILES 2>&1)
1007
+ RUFF_FMT_EXIT=$?
1008
+ if [ "$RUFF_FMT_EXIT" -ne 0 ]; then
1009
+ echo "$RUFF_FMT_OUTPUT" | head -20
1010
+ echo ""
1011
+ echo "❌ BLOCKED - Ruff format check failed"
1012
+ echo "Fix formatting: run 'ruff format .' to auto-format."
1013
+ exit 1
1014
+ fi
1015
+ echo "✅ PASSED - Ruff format check."
1016
+ fi
1017
+ fi
1018
+
1019
+ # Debug statement detection (breakpoint/pdb/ipdb)
1020
+ PY_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
1021
+ if [ -n "$PY_DEBUG_FILES" ]; then
1022
+ DEBUG_HITS=$(grep -n 'breakpoint()\|pdb\.set_trace()\|ipdb\.set_trace()\|import pdb\|import ipdb' $PY_DEBUG_FILES 2>/dev/null || true)
1023
+ if [ -n "$DEBUG_HITS" ]; then
1024
+ echo ""
1025
+ echo "❌ BLOCKED - Debug statements detected in Python files:"
1026
+ echo "$DEBUG_HITS" | head -10
1027
+ echo ""
1028
+ echo "Remove breakpoint()/pdb.set_trace() before committing."
1029
+ exit 1
1030
+ fi
1031
+ fi
968
1032
 
969
1033
  # Optional: Type checking with mypy
970
1034
  if command -v mypy &> /dev/null; then
@@ -1018,19 +1082,19 @@ else
1018
1082
  if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
1019
1083
  BASELINE_WARNINGS=$(echo "$SHELL_FILES" | node -e "
1020
1084
  const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
1021
- const stdin=require('fs').readFileSync('/dev/stdin','utf8').trim();
1085
+ const stdin=require('fs').readFileSync(0,'utf8').trim();
1022
1086
  const files=stdin?stdin.split('\n'):[];
1023
1087
  let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
1024
1088
  console.log(total);
1025
1089
  ")
1026
1090
  CURRENT_WARNINGS=$(echo "$SHELLCHECK_OUTPUT" | node -e "
1027
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
1091
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
1028
1092
  console.log(d.length);
1029
1093
  ")
1030
1094
  if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
1031
1095
  NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
1032
1096
  echo "$SHELLCHECK_OUTPUT" | node -e "
1033
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
1097
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
1034
1098
  for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
1035
1099
  " | head -30
1036
1100
  echo ""
@@ -1047,7 +1111,7 @@ else
1047
1111
  fi
1048
1112
  else
1049
1113
  echo "$SHELLCHECK_OUTPUT" | node -e "
1050
- const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
1114
+ const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
1051
1115
  for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
1052
1116
  " | head -30
1053
1117
  echo ""
@@ -1476,6 +1540,13 @@ else
1476
1540
  # ========================================================================
1477
1541
  CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)
1478
1542
 
1543
+ # Strip PROJECT_SUBDIR prefix from test file paths when running in subdirectory.
1544
+ # git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR" (line 728)
1545
+ # vitest and [ -f ] checks need subdirectory-relative paths. (v0.14.31+ fix)
1546
+ if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_TEST_FILES" ]; then
1547
+ CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
1548
+ fi
1549
+
1479
1550
  if [ -n "$CHANGED_TEST_FILES" ]; then
1480
1551
  echo "Checking mock density in test files..."
1481
1552
  for test_file in $CHANGED_TEST_FILES; do
@@ -1563,6 +1634,10 @@ else
1563
1634
 
1564
1635
  # Also get changed source files (non-test) for related-test lookup
1565
1636
  CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
1637
+ # Strip PROJECT_SUBDIR prefix (same as CHANGED_TEST_FILES fix above)
1638
+ if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_SRC_FILES" ]; then
1639
+ CHANGED_SRC_FILES=$(echo "$CHANGED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
1640
+ fi
1566
1641
  CHANGED_SRC_COUNT=$(echo "$CHANGED_SRC_FILES" | grep -c '.' || echo "0")
1567
1642
 
1568
1643
  # Decide strategy
@@ -2447,24 +2522,7 @@ ENDJSON
2447
2522
  REPORT_TS=$(date -u +"%Y-%m-%d-%H%M%S")
2448
2523
  local PERSISTED_REPORT_FILE="${REPORTS_DIR}/${REPORT_TS}.json"
2449
2524
  mkdir -p "$REPORTS_DIR"
2450
- python3 - <<PY 2>/dev/null || cp "$REPORT_FILE" "$PERSISTED_REPORT_FILE"
2451
- import json
2452
- from pathlib import Path
2453
- branch_report = json.loads(Path('$REPORT_FILE').read_text())
2454
- changed_files = '''${CHANGED_FILES:-}'''.splitlines()
2455
- report = {
2456
- 'timestamp': '$TIMESTAMP',
2457
- 'trigger': 'pre-commit',
2458
- 'branch': '$BRANCH',
2459
- 'commit': '$COMMIT_HASH',
2460
- 'changed_files': [f for f in changed_files if f],
2461
- 'overall': branch_report.get('overall', {}),
2462
- 'gates': branch_report.get('gates', {}),
2463
- 'warnings': [],
2464
- 'errors': [],
2465
- }
2466
- Path('$PERSISTED_REPORT_FILE').write_text(json.dumps(report, indent=2) + '\n')
2467
- PY
2525
+ _XP_RF="$REPORT_FILE" _XP_PRF="$PERSISTED_REPORT_FILE" _XP_TS="$TIMESTAMP" _XP_BR="$BRANCH" _XP_CH="$COMMIT_HASH" _XP_CF="${CHANGED_FILES:-}" node -e "const fs=require('fs');const e=process.env;const br=JSON.parse(fs.readFileSync(e._XP_RF,'utf8'));const cf=(e._XP_CF||'').split('\n').filter(f=>f);const r={timestamp:e._XP_TS,trigger:'pre-commit',branch:e._XP_BR,commit:e._XP_CH,changed_files:cf,overall:br.overall||{},gates:br.gates||{},warnings:[],errors:[]};fs.writeFileSync(e._XP_PRF,JSON.stringify(r,null,2)+'\n');" 2>/dev/null || cp "$REPORT_FILE" "$PERSISTED_REPORT_FILE"
2468
2526
 
2469
2527
  # ── History append (unchanged — append-only for trend) ────────────────────────────
2470
2528
  local GATES_JSON="{"
package/hooks/pre-push CHANGED
@@ -27,39 +27,7 @@ write_pre_push_report() {
27
27
  commit_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
28
28
  verdict=$([ "$exit_code" -eq 0 ] && echo "PASS" || echo "BLOCKED")
29
29
  mkdir -p "$reports_dir"
30
- python3 - <<PY 2>/dev/null || true
31
- import json
32
- from pathlib import Path
33
- pushed_files = '''${PUSHED_FILES:-}'''.splitlines()
34
- report = {
35
- 'timestamp': '$timestamp',
36
- 'trigger': 'pre-push',
37
- 'branch': '$CURRENT_BRANCH',
38
- 'commit': '$commit_hash',
39
- 'changed_files': [f for f in pushed_files if f],
40
- 'overall': {
41
- 'score': '10.0/10' if '$verdict' == 'PASS' else '0.0/10',
42
- 'gates_passed': 1 if '$verdict' == 'PASS' else 0,
43
- 'gates_total': 1,
44
- 'verdict': '$verdict',
45
- },
46
- 'gates': [
47
- {'id': '10', 'name': 'Build Integrity', 'status': '${GATE_10_STATUS:-SKIP}', 'details': {}},
48
- {'id': 'M', 'name': 'Mutation Testing', 'status': '${GATE_M_STATUS:-SKIP}', 'details': {}},
49
- {'id': 'M-Python', 'name': 'Mutation Testing (Python)', 'status': '${GATE_M_PYTHON_STATUS:-SKIP}', 'details': {}},
50
- {'id': 'M-Go', 'name': 'Mutation Testing (Go)', 'status': '${GATE_M_GO_STATUS:-SKIP}', 'details': {}},
51
- {'id': 'M-Java', 'name': 'Mutation Testing (Java)', 'status': '${GATE_M_JAVA_STATUS:-SKIP}', 'details': {}},
52
- {'id': 'M-Kotlin', 'name': 'Mutation Testing (Kotlin)', 'status': '${GATE_M_KOTLIN_STATUS:-SKIP}', 'details': {}},
53
- {'id': 'MD', 'name': 'Mock Density', 'status': '${GATE_M2_STATUS:-SKIP}', 'details': {}},
54
- {'id': 'ML', 'name': 'Mock Layering', 'status': '${GATE_M3_STATUS:-SKIP}', 'details': {}},
55
- {'id': 'UI', 'name': 'UI Sprint Gates', 'status': '${GATE_UI_STATUS:-SKIP}', 'details': {}},
56
- {'id': 'MW', 'name': 'Code Walkthrough', 'status': '${GATE_DELPHI_STATUS:-SKIP}', 'details': {'remote': '$REMOTE', 'url': '$URL'}}
57
- ],
58
- 'warnings': [],
59
- 'errors': [] if '$verdict' == 'PASS' else ['pre-push hook exited with code $exit_code'],
60
- }
61
- Path('$report_file').write_text(json.dumps(report, indent=2) + '\n')
62
- PY
30
+ _XP_RF="$report_file" _XP_TS="$timestamp" _XP_BR="$CURRENT_BRANCH" _XP_CH="$commit_hash" _XP_PF="${PUSHED_FILES:-}" _XP_VD="$verdict" _XP_EC="$exit_code" _XP_G10="${GATE_10_STATUS:-SKIP}" _XP_GM="${GATE_M_STATUS:-SKIP}" _XP_GMP="${GATE_M_PYTHON_STATUS:-SKIP}" _XP_GMGO="${GATE_M_GO_STATUS:-SKIP}" _XP_GMJA="${GATE_M_JAVA_STATUS:-SKIP}" _XP_GMKO="${GATE_M_KOTLIN_STATUS:-SKIP}" _XP_GMD="${GATE_M2_STATUS:-SKIP}" _XP_GML="${GATE_M3_STATUS:-SKIP}" _XP_GUI="${GATE_UI_STATUS:-SKIP}" _XP_GMW="${GATE_DELPHI_STATUS:-SKIP}" _XP_RM="$REMOTE" _XP_URL="$URL" node -e "const fs=require('fs');const e=process.env;const pf=(e._XP_PF||'').split('\n').filter(f=>f);const r={timestamp:e._XP_TS,trigger:'pre-push',branch:e._XP_BR,commit:e._XP_CH,changed_files:pf,overall:{score:e._XP_VD==='PASS'?'10.0/10':'0.0/10',gates_passed:e._XP_VD==='PASS'?1:0,gates_total:1,verdict:e._XP_VD},gates:[{id:'10',name:'Build Integrity',status:e._XP_G10,details:{}},{id:'M',name:'Mutation Testing',status:e._XP_GM,details:{}},{id:'M-Python',name:'Mutation Testing (Python)',status:e._XP_GMP,details:{}},{id:'M-Go',name:'Mutation Testing (Go)',status:e._XP_GMGO,details:{}},{id:'M-Java',name:'Mutation Testing (Java)',status:e._XP_GMJA,details:{}},{id:'M-Kotlin',name:'Mutation Testing (Kotlin)',status:e._XP_GMKO,details:{}},{id:'MD',name:'Mock Density',status:e._XP_GMD,details:{}},{id:'ML',name:'Mock Layering',status:e._XP_GML,details:{}},{id:'UI',name:'UI Sprint Gates',status:e._XP_GUI,details:{}},{id:'MW',name:'Code Walkthrough',status:e._XP_GMW,details:{remote:e._XP_RM,url:e._XP_URL}}],warnings:[],errors:e._XP_VD==='PASS'?[]:['pre-push hook exited with code '+e._XP_EC]};fs.writeFileSync(e._XP_RF,JSON.stringify(r,null,2)+'\n');" 2>/dev/null || true
63
31
  }
64
32
 
65
33
  _pre_push_report_on_exit() {