#!/bin/bash
# OpenCode Quality Gates - Pre-Commit Hook - 12 numbered gates (Gate 0–11)
# 
# DESIGN PRINCIPLE: Tool unavailable = SKIP (graceful degradation), tool available + check fails = BLOCK
# Missing tools do NOT block the commit — the gate degrades to SKIP. Block fires only when
# the tool exists AND the check fails. This prevents broken toolchains from blocking work.
#
# Sourced from adapter-common.sh and language-specific adapters

# Source the common adapter functions — 3-tier resolution:
# 1. Global: ~/.config/xp-gate/adapters (for core.hooksPath global setup)
# 2. Project-local: <repo>/githooks/ (for per-project init)
# 3. Script dir: hooks directory containing this script (fallback)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ADAPTER_DIR=""
GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"

# BEGIN GIT CONTEXT FALLBACK
if ! declare -F run_without_git_context >/dev/null 2>&1; then
  run_without_git_context() (
    unset GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_CONFIG GIT_CONFIG_PARAMETERS
    unset GIT_CONFIG_COUNT GIT_OBJECT_DIRECTORY GIT_DIR GIT_WORK_TREE
    unset GIT_IMPLICIT_WORK_TREE GIT_GRAFT_FILE GIT_INDEX_FILE
    unset GIT_NO_REPLACE_OBJECTS GIT_REPLACE_REF_BASE GIT_PREFIX
    unset GIT_SHALLOW_FILE GIT_COMMON_DIR
    "$@"
  )
fi
# END GIT CONTEXT FALLBACK

PROJECT_GITHOOKS="$(run_without_git_context git rev-parse --show-toplevel 2>/dev/null)/githooks"
GATES_DIR="$SCRIPT_DIR/gates"

if [ -f "$GLOBAL_ADAPTER_DIR/adapter-common.sh" ]; then
  ADAPTER_DIR="$GLOBAL_ADAPTER_DIR"
elif [ -f "$PROJECT_GITHOOKS/adapter-common.sh" ]; then
  ADAPTER_DIR="$PROJECT_GITHOOKS"
else
  ADAPTER_DIR="$SCRIPT_DIR"
fi
source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
  echo "Error: Cannot source adapter-common.sh from $ADAPTER_DIR"
  echo "Run: xp-gate init (per-project) or xp-gate setup-global (all projects)"
  exit 1
}

# Resolve adapter file path — handles both layouts:
#   Global:      ~/.config/xp-gate/adapters/typescript.sh      (flat)
#   Project:     <repo>/githooks/adapters/typescript.sh          (nested)
#   Script dir:  <hook-dir>/adapters/typescript.sh               (nested)
# ADAPTER_DIR points to the base directory that contains either
# adapter-common.sh directly (global) or adapter-common.sh in a flat layout.
# Gate scripts live alongside adapter-common.sh.
resolve_adapter_path() {
  local lang="$1"
  # Try flat layout first (global: typescript.sh next to adapter-common.sh)
  if [ -f "$ADAPTER_DIR/${lang}.sh" ]; then
    echo "$ADAPTER_DIR/${lang}.sh"
    return 0
  fi
  # Try nested layout (project: adapters/typescript.sh)
  if [ -f "$ADAPTER_DIR/adapters/${lang}.sh" ]; then
    echo "$ADAPTER_DIR/adapters/${lang}.sh"
    return 0
  fi
  # Try project githooks as fallback
  if [ -f "$PROJECT_GITHOOKS/adapters/${lang}.sh" ]; then
    echo "$PROJECT_GITHOOKS/adapters/${lang}.sh"
    return 0
  fi
  # Try script dir as last resort
  if [ -f "$SCRIPT_DIR/adapters/${lang}.sh" ]; then
    echo "$SCRIPT_DIR/adapters/${lang}.sh"
    return 0
  fi
  return 1
}

# Gate scripts directory — falls back to project githooks if ADAPTER_DIR doesn't have them
if [ -f "$ADAPTER_DIR/gate-3.sh" ]; then
  GATE_DIR="$ADAPTER_DIR"
elif [ -f "$PROJECT_GITHOOKS/gate-3.sh" ]; then
  GATE_DIR="$PROJECT_GITHOOKS"
elif [ -f "$SCRIPT_DIR/gate-3.sh" ]; then
  GATE_DIR="$SCRIPT_DIR"
else
  GATE_DIR="$ADAPTER_DIR"
fi

# Trap: ensure quality report generated on ANY exit (pass or fail).
# Using a guard prevents recursion when 'exit' is called from inside the trap.
# IMPORTANT: Captures exit status FIRST so generate_quality_report cannot override it.
_QUALITY_REPORT_DONE=0
_quality_report_on_exit() {
  local _saved_exit=$?
  if [ "$_QUALITY_REPORT_DONE" = "1" ]; then
    exit $_saved_exit
    return
  fi
  _QUALITY_REPORT_DONE=1
  command -v generate_quality_report >/dev/null 2>&1 && generate_quality_report 2>/dev/null || true
  exit $_saved_exit
}
trap '_quality_report_on_exit' EXIT

PROJECT_ROOT="$(run_without_git_context git rev-parse --show-toplevel 2>/dev/null || echo "$(cd "$SCRIPT_DIR/../.." 2>/dev/null || echo "$SCRIPT_DIR/..")" )"
if [[ -d "$PROJECT_ROOT/node_modules/.bin" ]]; then
  export PATH="$PROJECT_ROOT/node_modules/.bin:$PATH"
fi

# Ensure pipe failures propagate exit codes through | head/tail/grep
set -o pipefail

# ============================================================================
# Helper Functions
# ============================================================================

# Parse LCOV file and return coverage percentage
# Usage: parse_lcov_coverage <lcov_file>
# Returns: coverage percentage (e.g., "85.5")
parse_lcov_coverage() {
  local lcov_file="$1"
  local total_lines=0
  local covered_lines=0
  
  if [ ! -f "$lcov_file" ]; then
    echo "0"
    return
  fi
  
  # Extract LF (Lines Found) and LH (Lines Hit) from lcov.info
  while IFS= read -r line; do
    case "$line" in
      LF:*)
        total_lines=$(echo "$line" | sed 's/LF://')
        ;;
      LH:*)
        covered_lines=$(echo "$line" | sed 's/LH://')
        ;;
    esac
  done < "$lcov_file"
  
  if [ "$total_lines" -gt 0 ]; then
    # Calculate percentage using awk for floating point
    awk "BEGIN {printf \"%.1f\", ($covered_lines / $total_lines) * 100}"
  else
    echo "0"
  fi
}

# Run command in subdirectory if PROJECT_SUBDIR is set
# Usage: run_in_subdir <command>
run_in_subdir() {
  local cmd="$1"
  
  if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ]; then
    # Run command in subdirectory, then return to original directory
    pushd "$PROJECT_SUBDIR" > /dev/null 2>&1
    eval "$cmd"
    local exit_code=$?
    popd > /dev/null 2>&1
    return $exit_code
  else
    # Run in current directory
    eval "$cmd"
    return $?
  fi
}

# Check if file exists in subdirectory
# Usage: file_exists_in_subdir <filename>
file_exists_in_subdir() {
  local filename="$1"
  
  if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ]; then
    [ -f "$PROJECT_SUBDIR/$filename" ]
  else
    [ -f "$filename" ]
  fi
}

# ============================================================================
# Audit/Timing helpers — must be defined BEFORE first gate call (line ~197)
# Bash executes top-to-bottom; gate_start_ms + record_gate_audit are called
# in every gate, so their definitions lead the first call.
# ============================================================================
AUDIT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AUDIT_SCRIPT_DIR/../..")"

# Issue #370: source cross-platform now_ms() helper (node-preferred, regex-validated)
source "${AUDIT_SCRIPT_DIR}/lib/now-ms.sh"

# Read max_duration_ms from .xp-gate-config.json (default 7200000 = 2 hours)
_MAX_DURATION_MS=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('${PROJECT_ROOT_FOR_AUDIT}/.xp-gate-config.json','utf8'));console.log((c.audit&&c.audit.max_duration_ms)||7200000);}catch(e){console.log(7200000);}" 2>/dev/null || echo "7200000")

# Get milliseconds timestamp (used by gates for duration measurement)
# Issue #370: delegates to now_ms() — node-preferred, regex-validated, cross-platform
gate_start_ms() {
  now_ms
}

# Audit helper — records gate execution to .xp-gate/audit.jsonl
# NEVER blocks commit on failure (try/catch around npx tsx).
# Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
record_gate_audit() {
  local gate_id="$1"
  local gate_name="$2"
  local passed="$3"
  local issues_found="$4"
  local start_ms="$5"
  local end_ms
  end_ms=$(now_ms)
  local duration_ms=$((end_ms - start_ms))
  if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
    duration_ms=0
  fi

  # Issue #370: detect anomalous duration (poisoned timestamp arithmetic)
  local duration_anomaly_flag=""
  if [ "$duration_ms" -gt "$_MAX_DURATION_MS" ] 2>/dev/null; then
    duration_anomaly_flag="--duration-anomaly true"
  fi

  # Determine gate result: PASS or FAIL based on status variable
  local gate_result="true"
  if [ "$passed" != "PASS" ] && [ "$passed" != "SKIP" ]; then
    gate_result="false"
  fi

  # Fire-and-forget: audit must never block the commit
  {
    npx tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
      --gate-id "$gate_id" \
      --gate-name "$gate_name" \
      --passed "$gate_result" \
      --issues-found "$issues_found" \
      --duration-ms "$duration_ms" \
      $duration_anomaly_flag \
      --trigger commit \
      2>/dev/null
  } &
  disown 2>/dev/null || true
}

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "   QUALITY GATES - PRE-COMMIT CHECK"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Refactored Structure: 6 Type-Based Gates"
echo ""

# ============================================================================
# Gate 0: Version Consistency Check (Protected Branches)
# ============================================================================
GATE_0_START=$(gate_start_ms)
GATE_0_STATUS="PASS"

CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
PROTECTED_BRANCHES="master develop trunk mainline"
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")

# Optional: project-level .protected-branches config file
if [ -f "$ROOT_DIR/.protected-branches" ]; then
  PROTECTED_BRANCHES="$PROTECTED_BRANCHES $(cat "$ROOT_DIR/.protected-branches" | tr '\n' ' ')"
fi

is_protected=false
for branch in $PROTECTED_BRANCHES; do
  if [ "$CURRENT_BRANCH" = "$branch" ]; then
    is_protected=true
    break
  fi
done

has_version_change=false
has_changelog_change=false

if [ "$is_protected" = "true" ]; then
  # Env var bypass: SKIP_VERSION_CHECK=1 skips Gate 0 entirely
  # Useful for: SKIP_VERSION_CHECK=1 git commit -m "chore: ..."
  if [ "${SKIP_VERSION_CHECK:-}" = "1" ]; then
    echo "⏭️  SKIPPED - Gate 0: Version consistency (SKIP_VERSION_CHECK=1 env var)"
  else
  # Check if staged changes include VERSION or CHANGELOG.md
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
  for file in $STAGED_FILES; do
    case "$file" in
      VERSION) has_version_change=true ;;
      CHANGELOG.md) has_changelog_change=true ;;
    esac
  done

  # If either VERSION or CHANGELOG.md is staged, pass
  if [ "$has_version_change" = "true" ] || [ "$has_changelog_change" = "true" ]; then
    # Auto-run sync-version when VERSION is staged (issue #206 follow-up).
    # Propagates VERSION → 4 package.json + refreshes AGENTS.md headers,
    # then stages only files sync-version is known to touch (NOT runtime
    # journals or unrelated edits) so the commit captures it atomically.
    # Prefer Node.js (.cjs) for cross-platform; fall back to bash (.sh).
    SYNC_VERSION_CMD=""
    if [ -f "$ROOT_DIR/scripts/sync-version.cjs" ] && command -v node >/dev/null 2>&1; then
      SYNC_VERSION_CMD="node $ROOT_DIR/scripts/sync-version.cjs"
    elif [ -f "$ROOT_DIR/scripts/sync-version.sh" ]; then
      SYNC_VERSION_CMD="bash $ROOT_DIR/scripts/sync-version.sh"
    fi
    if [ "$has_version_change" = "true" ] && [ -n "$SYNC_VERSION_CMD" ]; then
      echo "🔄 Gate 0: VERSION staged — running sync-version to fan out to package.json + AGENTS.md headers..."
      if $SYNC_VERSION_CMD > /tmp/xp-gate-sync-version.log 2>&1; then
        SYNC_STAGED=0
        # Stage the 4 known package.json targets if modified
        for pkg in package.json src/npm-package/package.json \
                   plugins/claude-code/.claude-plugin/plugin.json \
                   plugins/opencode/package.json \
                   src/npm-package/plugins/claude-code/.claude-plugin/plugin.json \
                   src/npm-package/plugins/opencode/package.json; do
          if [ -f "$ROOT_DIR/$pkg" ] && ! git diff --quiet -- "$ROOT_DIR/$pkg" 2>/dev/null; then
            git add "$ROOT_DIR/$pkg"
            SYNC_STAGED=$((SYNC_STAGED + 1))
          fi
        done
        # Stage AGENTS.md files with header-only modifications (header refresh is the only thing sync-version touches in AGENTS.md)
        while IFS= read -r -d '' agents_file; do
          if ! git diff --quiet -- "$agents_file" 2>/dev/null; then
            git add "$agents_file"
            SYNC_STAGED=$((SYNC_STAGED + 1))
          fi
        done < <(find "$ROOT_DIR" -name 'AGENTS.md' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0)
        echo "   auto-staged $SYNC_STAGED file(s) from sync-version (package.json × ≤6 + AGENTS.md headers)"
      else
        echo "⚠️  sync-version failed; see /tmp/xp-gate-sync-version.log — continuing without auto-sync"
      fi
    fi
    echo "✅ PASSED - Gate 0: Version Consistency Check (VERSION/CHANGELOG staged)"
  else
    # Check for doc-only changes (no source code files)
    has_source=false
    for file in $STAGED_FILES; do
      case "$file" in
        *.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
          has_source=true
          ;;
      esac
    done

    # Check for build-tooling-only changes (adapters, scripts, hooks)
    has_real_source=false
    for file in $STAGED_FILES; do
      case "$file" in
        src/npm-package/adapters/*|src/npm-package/scripts/*|src/npm-package/hooks/*|scripts/*|githooks/adapters/*)
          continue ;;
        *.ts|*.js|*.tsx|*.jsx|*.py|*.go|*.java|*.rs|*.c|*.cpp|*.h|*.css|*.scss)
          has_real_source=true
          ;;
      esac
    done

    # Doc-only changes: no source code files
    if [ "$has_source" = "false" ]; then
      echo "✅ PASSED - Gate 0: Version Consistency Check (doc-only changes)"
    else
      # Check for bypass: commit message prefix (read from COMMIT_EDITMSG during pre-commit hook)
      COMMIT_MSG=$(cat "$(git rev-parse --git-dir)/COMMIT_EDITMSG" 2>/dev/null | head -1 || echo "")
      if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\]'; then
        if echo "$COMMIT_MSG" | grep -qE '^\[skip-version-check\].*(chore:|docs:|release:)'; then
          if [ "$has_real_source" = "false" ]; then
            echo "✅ PASSED - Gate 0: Version Consistency Check (bypass: chore/docs/release)"
          else
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  VERSION CONSISTENCY CHECK (Gate 0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Branch: $CURRENT_BRANCH
ERROR: [skip-version-check] bypass used but production source files detected.

Bypass only allows build-tooling changes (adapters/, scripts/, hooks/).
Production source files require VERSION/CHANGELOG update.

Files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
"
            exit 1
          fi
        else
          echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  VERSION CONSISTENCY CHECK (Gate 0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Branch: $CURRENT_BRANCH
ERROR: [skip-version-check] bypass prefix invalid.

Allowed prefixes: chore:, docs:, release:
Example: [skip-version-check] chore: update dependencies
"
          exit 1
        fi
      else
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  VERSION CONSISTENCY CHECK (Gate 0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Branch: $CURRENT_BRANCH
ERROR: Committing to a protected branch without VERSION/CHANGELOG update.

This bypasses the ship workflow and causes version drift.
Before committing:
  1. Update VERSION file with new version
  2. Add entry to CHANGELOG.md
  3. Run: node scripts/sync-version.cjs

Or use --no-verify (discouraged) or include [skip-version-check] in commit message.

Staged files: $(echo "$STAGED_FILES" | tr '\n' ', ' | sed 's/,$//')
"
        GATE_0_STATUS="BLOCK"
        record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "1" "$GATE_0_START"
        exit 1
      fi
    fi
  fi
  fi  # closes SKIP_VERSION_CHECK if/else
else
  echo "✅ PASSED - Gate 0: Version Consistency Check (non-protected branch: $CURRENT_BRANCH)"
fi

record_gate_audit "gate-0" "version-consistency" "$GATE_0_STATUS" "0" "$GATE_0_START"

echo ""

# Get list of changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$CHANGED_FILES" ]; then
  echo "No files changed. Skipping gates."
  exit 0
fi

any_changed_files_match() {
  local pattern

  for pattern in "$@"; do
    if printf '%s\n' "$CHANGED_FILES" | grep -qE -- "$pattern"; then
      return 0
    fi
  done
  return 1
}

# ============================================================================
# Multi-language detection (v0.15.0+): detect all languages from changed file
# extensions, with fallback to manifest-based detection.
# Result: PROJECT_LANGS (space-separated string), PROJECT_LANG (first language for
# compatibility), PROJECT_SUBDIR (derived from changed file paths in monorepos;
# v0.15.1+; ARCH-03 fix), PROJECT_SUBDIRS (per-language subdir override, empty = root).
# ============================================================================

# Language override resolution (project-level > git config > env var)
# Priority: .xp-gate-lang file > git config xp-gate.lang > XP_GATE_LANG env
_RESOLVED_LANG_OVERRIDE=""
if [ -f ".xp-gate-lang" ]; then
  _RESOLVED_LANG_OVERRIDE=$(sed -n '1p' .xp-gate-lang 2>/dev/null | tr -d '[:space:]')
elif git config xp-gate.lang >/dev/null 2>&1; then
  _RESOLVED_LANG_OVERRIDE=$(git config xp-gate.lang 2>/dev/null)
elif [ -n "${XP_GATE_LANG:-}" ]; then
  _RESOLVED_LANG_OVERRIDE="$XP_GATE_LANG"
  echo "⚠️  XP_GATE_LANG env var is deprecated for multi-project use."
  echo "   Prefer: echo '$XP_GATE_LANG' > .xp-gate-lang  OR  git config xp-gate.lang $XP_GATE_LANG"
fi

if [ -n "$_RESOLVED_LANG_OVERRIDE" ]; then
  PROJECT_LANGS="$_RESOLVED_LANG_OVERRIDE"
  PROJECT_LANG="$_RESOLVED_LANG_OVERRIDE"
  echo ""
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "   🔧 LANGUAGE OVERRIDE: $PROJECT_LANG"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo ""
elif [ -f ".xp-gate-config.json" ]; then
  # ---- Read from project config file (v0.15.2+) ----
  # .xp-gate-config.json is generated by `xp-gate init` or `xp-gate detect-languages`
  # It contains detected languages and their tool status.
  _CONFIG_LANGS=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('.xp-gate-config.json','utf8'));console.log(Object.keys(c.languages||{}).join(' '));}catch(e){console.log('');}" 2>/dev/null || echo "")
  if [ -n "$_CONFIG_LANGS" ]; then
    PROJECT_LANGS="$_CONFIG_LANGS"
    PROJECT_LANG=$(echo "$_CONFIG_LANGS" | awk '{print $1}')
    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "   📋 LANGUAGES FROM .xp-gate-config.json"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
    echo "Detected languages: $PROJECT_LANGS"
    echo ""
  else
    # Config file exists but no languages detected — fall through to extension detection
    _CONFIG_LANGS=""
  fi
fi

# If no languages from override or config, detect from changed files
if [ -z "$PROJECT_LANGS" ] && [ -z "$_CONFIG_LANGS" ]; then
  # ---- Extension → language mapping ----
  # Extracts unique languages from changed file extensions
  PROJECT_LANGS=""
  _detect_ext_lang() {
    for ext in "$@"; do
      if echo "$CHANGED_FILES" | grep -qE "\.${ext}$" 2>/dev/null; then
        echo "1"
        return 0
      fi
    done
    echo "0"
    return 1
  }

  # Check changed files for each language by extension
  if [ "$(_detect_ext_lang "ts" "tsx" "js" "jsx" "mjs")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS typescript"
  fi
  if [ "$(_detect_ext_lang "py")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS python"
  fi
  if [ "$(_detect_ext_lang "go")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS go"
  fi
  if [ "$(_detect_ext_lang "java")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS java"
  fi
  if [ "$(_detect_ext_lang "kt" "kts")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS kotlin"
  fi
  if [ "$(_detect_ext_lang "dart")" = "1" ]; then
    # Check for Flutter: if pubspec.yaml exists and contains flutter
    if [ -f "pubspec.yaml" ] && grep -q "flutter:" pubspec.yaml 2>/dev/null; then
      PROJECT_LANGS="$PROJECT_LANGS flutter"
    else
      PROJECT_LANGS="$PROJECT_LANGS dart"
    fi
  fi
  if [ "$(_detect_ext_lang "swift")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS swift"
  fi
  if [ "$(_detect_ext_lang "m" "mm")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS objectivec"
  fi
  if [ "$(_detect_ext_lang "cpp" "cxx" "cc" "c" "hpp" "h")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS cpp"
  fi
  if [ "$(_detect_ext_lang "sh")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS shell"
  fi
  if [ "$(_detect_ext_lang "ps1" "psm1")" = "1" ]; then
    PROJECT_LANGS="$PROJECT_LANGS powershell"
  fi
  # IaC: .tf, .tfvars, Dockerfile, docker-compose*, .yaml/.yml in kube context
  if echo "$CHANGED_FILES" | grep -qE '(\.tf$|\.tfvars$|Dockerfile|docker-compose)' 2>/dev/null; then
    PROJECT_LANGS="$PROJECT_LANGS iac"
  fi

  # Trim leading space
  PROJECT_LANGS="${PROJECT_LANGS# }"

  # ---- Fallback: manifest-based detection when no extension matches ----
  if [ -z "$PROJECT_LANGS" ]; then
    if [ -f "package.json" ]; then
      HAS_RN=$(grep -q "react-native" package.json 2>/dev/null && echo "yes" || echo "no")
      HAS_IOS=$([ -d "ios" ] && echo "yes" || echo "no")
      HAS_ANDROID=$([ -d "android" ] && echo "yes" || echo "no")
      if [ "$HAS_RN" = "yes" ] || { [ "$HAS_IOS" = "yes" ] && [ "$HAS_ANDROID" = "yes" ]; }; then
        PROJECT_LANGS="react-native"
      else
        PROJECT_LANGS="typescript"
      fi
    elif [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
      PROJECT_LANGS="python"
    elif [ -f "go.mod" ]; then
      PROJECT_LANGS="go"
    elif [ -f "pubspec.yaml" ]; then
      if grep -q "flutter:" pubspec.yaml 2>/dev/null || [ -f ".metadata" ]; then
        PROJECT_LANGS="flutter"
      else
        PROJECT_LANGS="dart"
      fi
    elif [ -f "pom.xml" ] || [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
      KOTLIN_FILES=$(find . -name "*.kt" -not -path "./.git/*" 2>/dev/null | wc -l)
      if [ "$KOTLIN_FILES" -gt 0 ]; then
        PROJECT_LANGS="kotlin"
      else
        PROJECT_LANGS="java"
      fi
    elif [ -f "CMakeLists.txt" ] || [ -f "Makefile" ]; then
      CPP_FILES=$(find . -maxdepth 2 \( -name "*.cpp" -o -name "*.cxx" -o -name "*.cc" \) -not -path "./.git/*" 2>/dev/null | head -1)
      OBJC_FILES=$(find . -maxdepth 2 \( -name "*.m" -o -name "*.mm" \) -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$CPP_FILES" ]; then
        PROJECT_LANGS="cpp"
      elif [ -n "$OBJC_FILES" ]; then
        PROJECT_LANGS="objectivec"
      fi
    elif [ -n "$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)" ]; then
      PROJECT_LANGS="powershell"
    fi
  fi

  # ---- Derive PROJECT_SUBDIR from changed file paths (v0.15.1+; ARCH-03 fix) ----
  # Extension-based detection sets PROJECT_LANGS but leaves PROJECT_SUBDIR empty.
  # In a monorepo where config files (tsconfig.json, pyproject.toml, ...) live in
  # subdirectories (e.g. packages/frontend/), we must derive the subdirectory from
  # the actual changed files so that Gates 1-9 run in the correct context.
  PROJECT_SUBDIR=""
  if [ -n "$PROJECT_LANGS" ]; then
    # Walk up from each changed file to find the nearest project marker.
    _detect_subdir() {
      local fdir
      fdir=$(dirname "$1")
      while [ "$fdir" != "." ] && [ "$fdir" != "/" ]; do
        # Skip node_modules, .git, and other non-project directories
        case "$fdir" in
          */node_modules/*) fdir="${fdir%/*}" ; continue ;;
          */.git/*) fdir="${fdir%/*}" ; continue ;;
        esac
        if [ -f "$fdir/tsconfig.json" ] || [ -f "$fdir/package.json" ] || \
           [ -f "$fdir/pyproject.toml" ] || [ -f "$fdir/setup.py" ] || \
           [ -f "$fdir/go.mod" ] || [ -f "$fdir/pubspec.yaml" ] || \
           [ -f "$fdir/pom.xml" ] || [ -f "$fdir/build.gradle" ] || \
           [ -f "$fdir/build.gradle.kts" ] || [ -f "$fdir/CMakeLists.txt" ]; then
          echo "$fdir"
          return 0
        fi
        case "$fdir" in
          */*) fdir="${fdir%/*}" ;;
          *) break ;;
        esac
      done
      return 1
    }
    _subdir_set=""
    while IFS= read -r _changed; do
      [ -z "$_changed" ] && continue
      _d=$(_detect_subdir "$_changed" 2>/dev/null) || true
      if [ -n "$_d" ] && [ "$_d" != "." ]; then
        case " $_subdir_set " in
          *" $_d "*) ;; # already seen
          *) _subdir_set="${_subdir_set} $_d" ;;
        esac
      fi
    done <<CHANGEDFILELIST
$CHANGED_FILES
CHANGEDFILELIST
    _subdir_count=$(echo "$_subdir_set" | awk '{ n=0; for(i=1;i<=NF;i++) n++; print n }')
    if [ "$_subdir_count" = "1" ]; then
      PROJECT_SUBDIR=$(echo "$_subdir_set" | awk '{print $1}')
      echo ""
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo "   📁 MULTILANGUAGE SUBDIR DETECTED FROM CHANGED FILES"
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo ""
      echo "Language: $PROJECT_LANGS"
      echo "Location: $PROJECT_SUBDIR/"
      echo ""
    fi
  fi

  # ---- Subdirectory fallback (for single-language projects in subdirectories) ----
  # Only used when extension-based detection found nothing
  if [ -z "$PROJECT_LANGS" ]; then
    # Search for TypeScript/JavaScript projects
    TSCONFIG_SUB=$(find . -maxdepth 2 -name "tsconfig.json" -not -path "./.git/*" -not -path "./node_modules/*" 2>/dev/null | head -1)
    if [ -n "$TSCONFIG_SUB" ]; then
      PROJECT_SUBDIR=$(dirname "$TSCONFIG_SUB")
      if [ -f "$PROJECT_SUBDIR/package.json" ] && grep -q "react-native" "$PROJECT_SUBDIR/package.json" 2>/dev/null; then
        PROJECT_LANGS="react-native"
      else
        PROJECT_LANGS="typescript"
      fi
    fi

    # Search for Python projects
    if [ -z "$PROJECT_LANGS" ]; then
      PYPROJECT_SUB=$(find . -maxdepth 2 -name "pyproject.toml" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$PYPROJECT_SUB" ]; then
        PROJECT_LANGS="python"
        PROJECT_SUBDIR=$(dirname "$PYPROJECT_SUB")
      fi
    fi

    # Search for Go projects
    if [ -z "$PROJECT_LANGS" ]; then
      GOMOD_SUB=$(find . -maxdepth 2 -name "go.mod" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$GOMOD_SUB" ]; then
        PROJECT_LANGS="go"
        PROJECT_SUBDIR=$(dirname "$GOMOD_SUB")
      fi
    fi

    # Search for Flutter/Dart projects
    if [ -z "$PROJECT_LANGS" ]; then
      PUBSPEC_SUB=$(find . -maxdepth 2 -name "pubspec.yaml" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$PUBSPEC_SUB" ]; then
        if grep -q "flutter:" "$PUBSPEC_SUB" 2>/dev/null || [ -f "$(dirname "$PUBSPEC_SUB")/.metadata" ]; then
          PROJECT_LANGS="flutter"
        else
          PROJECT_LANGS="dart"
        fi
        PROJECT_SUBDIR=$(dirname "$PUBSPEC_SUB")
      fi
    fi

    # Search for Java/Kotlin projects
    if [ -z "$PROJECT_LANGS" ]; then
      POM_SUB=$(find . -maxdepth 2 -name "pom.xml" -not -path "./.git/*" 2>/dev/null | head -1)
      GRADLE_SUB=$(find . -maxdepth 2 -name "build.gradle" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$POM_SUB" ] || [ -n "$GRADLE_SUB" ]; then
        MANIFEST_FILE="${POM_SUB:-$GRADLE_SUB}"
        PROJECT_SUBDIR=$(dirname "$MANIFEST_FILE")
        KOTLIN_COUNT=$(find "$PROJECT_SUBDIR" -name "*.kt" -not -path "*/.git/*" 2>/dev/null | wc -l)
        if [ "$KOTLIN_COUNT" -gt 0 ]; then
          PROJECT_LANGS="kotlin"
        else
          PROJECT_LANGS="java"
        fi
      fi
    fi

    # Search for C++ projects
    if [ -z "$PROJECT_LANGS" ]; then
      CMAKE_SUB=$(find . -maxdepth 2 -name "CMakeLists.txt" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$CMAKE_SUB" ]; then
        PROJECT_LANGS="cpp"
        PROJECT_SUBDIR=$(dirname "$CMAKE_SUB")
      fi
    fi

    # Search for React Native projects (ios/ + android/ directories)
    if [ -z "$PROJECT_LANGS" ]; then
      RN_SUB=$(find . -maxdepth 2 -type d -name "ios" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$RN_SUB" ]; then
        PROJECT_SUBDIR=$(dirname "$RN_SUB")
        if [ -d "$PROJECT_SUBDIR/android" ] || [ -f "$PROJECT_SUBDIR/package.json" ]; then
          PROJECT_LANGS="react-native"
        fi
      fi
    fi

    # Search for PowerShell projects
    if [ -z "$PROJECT_LANGS" ]; then
      PS_SUB=$(find . -maxdepth 2 -name "*.ps1" -not -path "./.git/*" 2>/dev/null | head -1)
      if [ -n "$PS_SUB" ]; then
        PROJECT_LANGS="powershell"
        PROJECT_SUBDIR=$(dirname "$PS_SUB")
      fi
    fi

    # Report detected subdirectory project
    if [ -n "$PROJECT_SUBDIR" ] && [ -n "$PROJECT_LANGS" ]; then
      echo ""
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo "   📁 SUBDIRECTORY PROJECT DETECTED"
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo ""
      echo "Language: $PROJECT_LANGS"
      echo "Location: $PROJECT_SUBDIR/"
      echo ""
    fi
  fi

  # Set PROJECT_LANG for backward compatibility (first language in list)
  PROJECT_LANG=$(echo "$PROJECT_LANGS" | awk '{print $1}')

  # ---- Documentation-only fallback ----
  if [ -z "$PROJECT_LANGS" ]; then
    CODE_FILES=$(find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.java" -o -name "*.go" -o -name "*.rs" -o -name "*.cpp" -o -name "*.c" -o -name "*.swift" -o -name "*.kt" -o -name "*.sh" -o -name "*.dart" -o -name "*.ps1" \) -not -path "./.git/*" 2>/dev/null | wc -l)

    if [ "$CODE_FILES" -eq 0 ]; then
      PROJECT_LANGS="documentation-only"
      PROJECT_LANG="documentation-only"
      echo ""
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo "   📚 DOCUMENTATION-ONLY PROJECT DETECTED"
      echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      echo ""
      echo "This project contains no source code files."
      echo "Skipping static analysis and test gates."
      echo ""
      echo "Proceeding with documentation-only checks..."
    fi
  fi

  # ---- Multi-language project banner ----
  _lang_count=$(echo "$PROJECT_LANGS" | wc -w)
  if [ "$_lang_count" -gt 1 ]; then
    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "   🌐 MULTI-LANGUAGE PROJECT DETECTED"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
    echo "Detected $_lang_count languages from changed files:"
    for _l in $PROJECT_LANGS; do
      echo "  • $_l"
    done
    echo ""
  fi
fi  # close language override if/else

# Helper: check if a language is in PROJECT_LANGS
has_project_lang() {
  for lang in $PROJECT_LANGS; do
    if [ "$lang" = "$1" ]; then return 0; fi
  done
  return 1
}
# Switch to subdirectory if detected (for single-language subdirectory projects).
# Multi-language projects stay at root — each adapter handles its own path.
ORIGINAL_DIR=""
_lang_count=$(echo "$PROJECT_LANGS" | wc -w)
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ "$PROJECT_LANG" != "documentation-only" ] && [ "$_lang_count" -le 1 ]; then
  ORIGINAL_DIR=$(pwd)
  cd "$PROJECT_SUBDIR"
  echo "Working in: $PROJECT_SUBDIR/"
  echo ""
fi

# ============================================================================
# GATE 1: Code Quality (Static Analysis + Linting + Shell Check combined)
# Combination of: Old Gates 1 (Static), Gate 2 (Linting), Gate 5 (Shell Check)
# v0.14.3+ (Issue #312): Skip if no code files changed
# ============================================================================
 2>&1 echo ""
 2>&1 echo "→ Gate 1: Code Quality (static analysis + linting)..."
if ! any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|mm|dart|sh)$" "\.ya?ml$" "\.json$" "Dockerfile" "\.tf$"; then
  echo "✅ PASSED - No code files changed, skipping lint."
else
GATE_1_START=$(gate_start_ms)

if [ "$PROJECT_LANG" = "documentation-only" ]; then
  # Documentation-only project - skip static analysis
  echo "⏭️  SKIPPED - Code quality (documentation project)."
  
else
  for CURRENT_LANG in $PROJECT_LANGS; do
    echo "   ── Checking $CURRENT_LANG ──"
    case "$CURRENT_LANG" in
      typescript)
  # TypeScript: static analysis (tsc) + linting (eslint)
  if ! command -v npx &> /dev/null; then
    echo "ℹ️  npx not available — skipping TypeScript checks"
    echo "⏭️  SKIPPED - Code quality (npx not available)"
  else
    # Verify tsc is real TypeScript compiler (npx may fetch deprecated tsc@2.0.4 placeholder)
    TSC_OUTPUT=$(npx tsc --version 2>&1)
    if echo "$TSC_OUTPUT" | grep -q "Version"; then
      # Only run tsc if tsconfig.json exists (without it tsc prints help and exits non-zero)
      if [ -f "tsconfig.json" ]; then
        # Run TypeScript type checking
        echo "Running TypeScript static analysis ($TSC_OUTPUT)..."
        npx tsc --noEmit --skipLibCheck 2>&1 | head -30
        TSC_EXIT=$?
        if [ "$TSC_EXIT" -ne 0 ]; then
          echo ""
          echo "❌ BLOCKED - TYPE ERRORS detected"
          echo "Fix the type errors above before committing."
          exit 1
        fi
        echo "✅ PASSED - TypeScript static analysis."
        
        # Also check test files if they exist (Issue #293)
        # Many projects exclude __tests__/ from tsconfig.json, so test file type
        # errors silently accumulate.
        if [ -d "src/__tests__" ] || [ -d "src/tests" ] || [ -d "tests" ] || [ -d "__tests__" ]; then
          HAS_TESTS_IN_TSC=false
          if npx tsc --noEmit --skipLibCheck --listFiles 2>/dev/null | grep -qE '__tests__/|\.test\.ts|\.spec\.ts'; then
            HAS_TESTS_IN_TSC=true
          fi
          if [ "$HAS_TESTS_IN_TSC" = false ]; then
            TSC_TEST_EXIT=0
            if [ -f "tsconfig.tests.json" ]; then
              echo "Checking test files with tsconfig.tests.json..."
              npx tsc --noEmit --project tsconfig.tests.json 2>&1 | head -30
              TSC_TEST_EXIT=$?
            else
              echo "Checking test files for type errors..."
              TEMP_TSCONFIG=".tsconfig.withtests.json"
              node -e "
                const cfg = JSON.parse(require('fs').readFileSync('tsconfig.json','utf8'));
                delete cfg.exclude;
                cfg.include = cfg.include || ['src/**/*'];
                cfg.include.push('src/**/__tests__/**','src/**/*.test.ts','src/**/*.spec.ts');
                require('fs').writeFileSync('$TEMP_TSCONFIG', JSON.stringify(cfg, null, 2));
              " 2>/dev/null
              npx tsc --noEmit --project "$TEMP_TSCONFIG" --skipLibCheck 2>&1 | head -30
              TSC_TEST_EXIT=$?
              rm -f "$TEMP_TSCONFIG"
            fi
            if [ $TSC_TEST_EXIT -ne 0 ]; then
              echo ""
              echo "❌ BLOCKED - TYPE ERRORS in test files"
              echo "Your project's tsconfig.json excludes test files from type checking."
              echo "Add a tsconfig.tests.json to customise test file checking."
              exit 1
            fi
          fi
        fi
      else
        echo "ℹ️  SKIP - tsconfig.json not found (no TypeScript project config)"
      fi
    else
      echo "ℹ️  SKIP - tsc not properly installed ($TSC_OUTPUT)"
      echo "Install typescript (npm install typescript --save-dev) for type checking."
    fi
    
    # Run ESLint linting if config exists
    if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
      echo "Running ESLint linting..."
      # Filter to only TS/JS files - ESLint config ignores non-matching files
      ESLINT_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
      if [ -n "$ESLINT_FILES" ]; then
        ESLINT_OUTPUT=$(npx eslint $ESLINT_FILES -f json --no-warn-ignored 2>&1)
        ESLINT_EXIT=$?
        if [ "$ESLINT_EXIT" -ne 0 ]; then
          if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
            # Compare against lint baseline
            BASELINE_WARNINGS=$(echo "$ESLINT_FILES" | tr ' ' '\n' | node -e "
              const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
              const stdin=require('fs').readFileSync(0,'utf8').trim();
              const files=stdin?stdin.split('\n'):[];
              let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
              console.log(total);
            ")
            CURRENT_WARNINGS=$(echo "$ESLINT_OUTPUT" | node -e "
              const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
              console.log(d.reduce((s,f)=>s+(f.warningCount||0)+(f.errorCount||0),0));
            ")
            if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
              NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
              echo "$ESLINT_OUTPUT" | head -30
              echo ""
              echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
              echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
              exit 1
            else
              REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
              if [ "$REDUCED" -gt 0 ]; then
                echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
              else
                echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
              fi
            fi
          else
            echo "$ESLINT_OUTPUT" | head -30
            echo ""
            echo "❌ BLOCKED - LINT ERRORS detected"
            echo "Fix the lint errors above before committing."
            echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
            exit 1
          fi
        else
          echo "✅ PASSED - ESLint linting."
        fi
      else
        echo "✅ PASSED - No staged TS/JS files to lint."
      fi
    else
      echo "ℹ️  No ESLint configuration found - Skipping"
      if [ -f "package.json" ] && command -v node &> /dev/null; then
        HAS_ESLINT=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d.eslint||deps.eslint)?'yes':'no')}catch{console.log('no')}") && true
        if [ "$HAS_ESLINT" = "yes" ]; then
          echo "⚠️  WARNING: eslint is installed in package.json but no eslint config found"
          echo "   Either create .eslintrc.* / eslint.config.* or remove eslint from dependencies"
          echo "   See: https://eslint.org/docs/latest/use/configure/configuration-files"
        fi
      fi
    fi

    # ── Biome lint/format check ──
    if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
      echo "Running Biome lint/format check..."
      if ! npx biome check --staged . 2>/dev/null; then
        npx biome check . 2>&1 | head -50
        echo ""
        echo "❌ BLOCKED - Biome check failed"
        echo "Fix the errors above before committing."
        echo "Tip: Run 'npx biome check --write .' to auto-fix formatting and safe lint issues."
        echo "     Run 'npx biome check --write --unsafe .' to also fix unsafe lint issues."
        exit 1
      fi
      echo "✅ PASSED - Biome check."
    else
      if [ -f "package.json" ] && command -v node &> /dev/null; then
        HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
        if [ "$HAS_BIOME" = "yes" ]; then
          echo ""
          echo "⚠️  WARNING: @biomejs/biome is installed in package.json but no biome.json found"
          echo "   Create a biome.json configuration file to enable Biome linting."
          echo "   NOTE: useLiteralKeys conflicts with TypeScript TS4111 — see docs/biome-configuration.md"
          echo "   For quick setup: add '\"linter\":{\"rules\":{\"complexity\":{\"useLiteralKeys\":\"off\"}}}' to biome.json"
        fi
      fi
    fi

    # Debug statement detection (debugger statement in JS/TS)
    TS_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx)$' || true)
    if [ -n "$TS_DEBUG_FILES" ]; then
      DEBUGGER_HITS=$(grep -n '^\s*debugger\s*;\?\s*$' $TS_DEBUG_FILES 2>/dev/null || true)
      if [ -n "$DEBUGGER_HITS" ]; then
        echo ""
        echo "❌ BLOCKED - 'debugger' statements detected:"
        echo "$DEBUGGER_HITS" | head -10
        echo ""
        echo "Remove debugger statements before committing."
        exit 1
      fi
    fi
  fi
    ;;
    
    python)
  # Python: Ruff (includes syntax + linting) + mypy (optional typing)
  if ! command -v ruff &> /dev/null; then
    echo "ℹ️  ruff not available — skipping Python linting"
    echo "⏭️  SKIPPED - Code quality (ruff not available, run: pip install ruff)"
  else
    # Run Ruff (includes syntax check, linting)
    echo "Running Ruff linting (syntax + lint)..."
    RUFF_OUTPUT=$(ruff check "$CHANGED_FILES" --output-format json 2>&1)
    RUFF_EXIT=$?
    if [ "$RUFF_EXIT" -ne 0 ]; then
      if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
        # Get baseline warnings for changed Python files
        PY_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
        if [ -n "$PY_FILES" ]; then
          BASELINE_WARNINGS=$(echo "$PY_FILES" | node -e "
            const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
            const stdin=require('fs').readFileSync(0,'utf8').trim();
            const files=stdin?stdin.split('\n'):[];
            let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
            console.log(total);
          ")
          CURRENT_WARNINGS=$(echo "$RUFF_OUTPUT" | node -e "
            const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
            console.log(d.reduce((s,f)=>s+(f.messages?f.messages.length:0),0));
          ")
          if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
            NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
            echo "$RUFF_OUTPUT" | node -e "
              const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
              for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
            " | head -30
            echo ""
            echo "❌ BLOCKED - ${NEW_WARNINGS} NEW lint error(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
            echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
            exit 1
          else
            REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
            if [ "$REDUCED" -gt 0 ]; then
              echo "✅ PASSED - Lint debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
            else
              echo "✅ PASSED - No new lint errors (baseline: ${BASELINE_WARNINGS})"
            fi
          fi
        else
          echo "✅ PASSED - Ruff linting (no Python files to check)."
        fi
      else
        echo "$RUFF_OUTPUT" | node -e "
          const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
          for(const f of d){for(const m of f.messages||[]){console.log(m.kind+': '+m.message)}}
        " | head -30
        echo ""
        echo "❌ BLOCKED - RUFF ERRORS detected"
        echo "Fix the lint errors above before committing."
        echo "Tip: Run 'ruff check --fix' to auto-fix some issues."
        echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
        exit 1
      fi
    else
      echo "✅ PASSED - Ruff linting."
    fi

    # Ruff format check (formatting gate)
    if command -v ruff &> /dev/null; then
      PY_FMT_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
      if [ -n "$PY_FMT_FILES" ]; then
        RUFF_FMT_OUTPUT=$(ruff format --check $PY_FMT_FILES 2>&1)
        RUFF_FMT_EXIT=$?
        if [ "$RUFF_FMT_EXIT" -ne 0 ]; then
          echo "$RUFF_FMT_OUTPUT" | head -20
          echo ""
          echo "❌ BLOCKED - Ruff format check failed"
          echo "Fix formatting: run 'ruff format .' to auto-format."
          exit 1
        fi
        echo "✅ PASSED - Ruff format check."
      fi
    fi

    # Debug statement detection (breakpoint/pdb/ipdb)
    PY_DEBUG_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
    if [ -n "$PY_DEBUG_FILES" ]; then
      DEBUG_HITS=$(grep -n 'breakpoint()\|pdb\.set_trace()\|ipdb\.set_trace()\|import pdb\|import ipdb' $PY_DEBUG_FILES 2>/dev/null || true)
      if [ -n "$DEBUG_HITS" ]; then
        echo ""
        echo "❌ BLOCKED - Debug statements detected in Python files:"
        echo "$DEBUG_HITS" | head -10
        echo ""
        echo "Remove breakpoint()/pdb.set_trace() before committing."
        exit 1
      fi
    fi
    
    # Optional: Type checking with mypy
    if command -v mypy &> /dev/null; then
      echo "Running mypy type checking (optional)..."
      mypy --ignore-missing-imports $(echo "$CHANGED_FILES" | grep "\.py$" || echo ".") 2>&1 | head -20 | tail -10
      MYPY_EXIT=$?
      if [ "$MYPY_EXIT" -ne 0 ]; then
        echo ""
        echo "❌ BLOCKED - MYPI TYPE ERRORS detected"
        echo "Fix the type errors above before committing."
        exit 1
      fi
      echo "✅ PASSED - mypy type checking."
    else
      echo "ℹ️  Mypy not available - Skipping optional type check"
    fi
  fi
    ;;
    
    go)
  # Go: go vet + golangci-lint
  if ! command -v golangci-lint &> /dev/null; then
    echo "ℹ️  golangci-lint not available — skipping Go linting"
    echo "⏭️  SKIPPED - Code quality (golangci-lint not available)"
  else
    echo "Running golangci-lint (comprehensive static analysis)..."
    golangci-lint run 2>&1 | head -30
    GOLANGCI_EXIT=$?
    if [ "$GOLANGCI_EXIT" -ne 0 ]; then
      echo ""
      echo "❌ BLOCKED - GOLANGCI-LINT ERRORS detected"
      echo "Fix the lint errors above before committing."
      exit 1
    fi
    echo "✅ PASSED - golangci-lint."
  fi
    ;;
    
    shell)
  # Shell: check staged shell files only. Installed hook adapters are infrastructure,
  # not project changes, and should not be linted on unrelated commits.
  SHELL_FILES=$(echo "$CHANGED_FILES" | grep '\.sh$' || true)

  if [ -z "$SHELL_FILES" ]; then
    echo "✅ PASSED - No staged shell scripts to check."
  elif command -v shellcheck &> /dev/null; then
    echo "Running shellcheck on staged shell scripts..."
    SHELLCHECK_OUTPUT=$(shellcheck -f json $SHELL_FILES 2>&1)
    SHELLCHECK_EXIT=$?
    if [ "$SHELLCHECK_EXIT" -ne 0 ]; then
      if [ -f ".xp-gate/lint-baseline.json" ] && command -v node &> /dev/null; then
        BASELINE_WARNINGS=$(echo "$SHELL_FILES" | node -e "
          const fs=require('fs');const b=JSON.parse(fs.readFileSync('.xp-gate/lint-baseline.json','utf8'));
          const stdin=require('fs').readFileSync(0,'utf8').trim();
          const files=stdin?stdin.split('\n'):[];
          let total=0;for(const f of files){if(b[f])total+=b[f].totalWarnings||0}
          console.log(total);
        ")
        CURRENT_WARNINGS=$(echo "$SHELLCHECK_OUTPUT" | node -e "
          const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
          console.log(d.length);
        ")
        if [ "$CURRENT_WARNINGS" -gt "$BASELINE_WARNINGS" ]; then
          NEW_WARNINGS=$((CURRENT_WARNINGS - BASELINE_WARNINGS))
          echo "$SHELLCHECK_OUTPUT" | node -e "
            const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
            for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
          " | head -30
          echo ""
          echo "❌ BLOCKED - ${NEW_WARNINGS} NEW shellcheck issue(s) (baseline: ${BASELINE_WARNINGS}, current: ${CURRENT_WARNINGS})"
          echo "Run 'xp-gate baseline reset' after fixing to update the baseline."
          exit 1
        else
          REDUCED=$((BASELINE_WARNINGS - CURRENT_WARNINGS))
          if [ "$REDUCED" -gt 0 ]; then
            echo "✅ PASSED - Shellcheck debt reduced by ${REDUCED} (${BASELINE_WARNINGS} → ${CURRENT_WARNINGS})"
          else
            echo "✅ PASSED - No new shellcheck issues (baseline: ${BASELINE_WARNINGS})"
          fi
        fi
      else
        echo "$SHELLCHECK_OUTPUT" | node -e "
          const d=JSON.parse(require('fs').readFileSync(0,'utf8'));
          for(const i of d){console.log(i.file+':'+i.line+': '+i.level+': '+i.message)}
        " | head -30
        echo ""
        echo "❌ BLOCKED - SHELLCHECK ERRORS detected"
        echo "Fix the shell script errors above."
        echo "Tip: Run 'xp-gate baseline create' to establish a lint baseline."
        exit 1
      fi
    else
      echo "✅ PASSED - Shellcheck completed."
    fi
  else
    echo "ℹ️  shellcheck not available - Attempting basic syntax check"
    # Basic check for staged shell script files
    for sh_file in $SHELL_FILES; do
      if [ -f "$sh_file" ]; then
        bash -n "$sh_file"
        if [ $? -ne 0 ]; then
          echo "❌ BLOCKED - SYNTAX ERROR in $sh_file"
          exit 1
        fi
      fi
    done
    echo "✅ PASSED - Basic shell script syntax check."
  fi
    ;;
    
    *)
      # Route to appropriate adapter for other languages
      ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
      if [ -n "$ADAPTER_FILE" ] && source "$ADAPTER_FILE" 2>/dev/null; then
        echo "Running static analysis for $CURRENT_LANG..."
        run_static_analysis | head -30 2>/dev/null
        ANALYSIS_EXIT=$?
        if [ "$ANALYSIS_EXIT" -ne 0 ]; then
          echo "⚠️  Static analysis had issues (may be non-blocking)"
        fi
    
        echo "Running lint check for $CURRENT_LANG..."
        run_lint | head -30 2>/dev/null
        LINT_EXIT=$?
        if [ "$LINT_EXIT" -ne 0 ]; then
          echo "ℹ️  Lint check had issues (may be non-blocking)"
        fi
    
        if [ "$ANALYSIS_EXIT" -eq 0 ] && [ "$LINT_EXIT" -eq 0 ]; then
          echo "✅ PASSED - Language-specific code quality checks."
        else
          # Tools unavailable or failed — skip non-blockingly
          echo "⏭️  SKIPPED - Code quality (tools unavailable or failed)"
        fi
      else
        # If no adapter exists for this language, skip
        echo "ℹ️  No specific adapter for $CURRENT_LANG — using generic checks if any"
        echo "⏭️  SKIPPED - Code quality (no specific checks for $CURRENT_LANG)"
      fi
      ;;
    esac
  done
fi
fi  # End: if ! any_changed_files_match (Issue #312 skip)
GATE_1_STATUS="PASS"
GATE_1_TOOL="${PROJECT_LANGS}"
record_gate_audit "gate-1" "code-quality" "$GATE_1_STATUS" "0" "$GATE_1_START"

# ============================================================================
# GATE 2: Duplicate Code Detection (NEW gate)
# Uses jscpd, or similar tools, specific to language
# ============================================================================
 2>&1 echo ""
 2>&1 echo "→ Gate 2: Duplicate code detection..."
GATE_2_START=$(gate_start_ms)

if [ "$PROJECT_LANG" = "documentation-only" ]; then
echo "⏭️  SKIPPED - Duplicate code (documentation project)."

else
  for CURRENT_LANG in $PROJECT_LANGS; do
    echo "   ── Checking $CURRENT_LANG ──"
    case "$CURRENT_LANG" in
      typescript)
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
    exit 1
  fi
  
  echo "Running jscpd for duplicate code detection..."
  jscpd --config jscpd.conf.json $(echo "$CHANGED_FILES" | tr '\n' ' ') 2>&1 | head -30
  JSCPD_EXIT=$?
  if [ "$JSCPD_EXIT" -ne 0 ]; then
    echo "ℹ️  jscpd found duplicated code (warning, not blocking by default)"
    echo "   Consider refactoring duplicate code blocks."
  fi
  echo "✅ PASSED - jscpd duplicate code check completed."
    ;;
    
    python)
  if command -v pylint &> /dev/null; then
    echo "Running pylint duplicate detection..."
    pylint --disable=all --enable=duplicate-code $(echo "$CHANGED_FILES" | grep "\.py$")
    DUPLICATE_EXIT=$?
    if [ "$DUPLICATE_EXIT" -ne 0 ]; then
      echo "ℹ️  pylint found duplicate code (warning, not typically blocking)"
    else
      echo "✅ PASSED - pylint found no duplicates."
    fi
  else
    echo "ℹ️  pylint not found — skipping Python duplicate code check."
    echo "   Install pylint: pip install pylint"
  fi
    ;;
    
    go)
  if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
    exit 1
  fi
  
  echo "Running jscpd for Go duplicate code detection..."
  FILES_TO_CHECK=$(echo "$CHANGED_FILES" | grep "\.go$" | head -20)
  if [ -n "$FILES_TO_CHECK" ]; then
    jscpd $FILES_TO_CHECK 2>&1 | head -10
  fi
  echo "✅ PASSED - Go duplicate code check completed."
    ;;
    
    powershell)
  echo "ℹ️  No PowerShell-native duplicate detector (jscpd does not support .ps1)"
  echo "⏭️  SKIPPED - Duplicate code (no tool for PowerShell)"
    ;;
    
    shell)
  echo "ℹ️  Duplicate detection not typically used for shell scripts"
  echo "⏭️  SKIPPED - Duplicate code (no tool for shell scripts)"
    ;;
    
    *)
  SOURCE_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|scala|kt|cpp|c|h|rs|php|dart|swift)$')
  if [ -n "$SOURCE_FILES" ]; then
    if ! require_tool "jscpd" "Gate 2" "npm install -D jscpd"; then
      exit 1
    fi
    echo "Running jscpd for duplicate code detection..."
    jscpd $SOURCE_FILES 2>&1 | head -15
    echo "✅ PASSED - jscpd check completed."
  else
    echo "✅ PASSED - No source files detected for duplicate checking."
  fi
    ;;
    esac
  done
fi
GATE_2_STATUS="PASS"
record_gate_audit "gate-2" "duplicate-code" "$GATE_2_STATUS" "0" "$GATE_2_START"

# ============================================================================
# GATE 3: Cyclomatic Complexity Check
# Extracted to gate-3.sh for maintainability
# ============================================================================
source "$GATE_DIR/gate-3.sh"

# ============================================================================
# GATE 4: Principles Checker (Clean Code + SOLID)
# Extracted to gate-4.sh for maintainability
# ============================================================================
source "$GATE_DIR/gate-4.sh"

# ============================================================================
# GATE 5: Tests & Coverage Combined (Combines Old Gates 3 - Tests and 4 - Coverage)
# Uses adapter system to run tests + coverage for language
# 
# v0.14.3+ (Issue #312): Skip test run when no code files changed.
# Code files = src/**/*.ts, plugins/**/*.ts, *.ts, *.tsx, etc. (language-aware).
# VERSION, CHANGELOG, docs, config-only changes → SKIP test run entirely.
  # Gate 5a (new file pairing), 5b (mock density), and 5c (semantic annotation)
  # are lightweight and still run even when no code files changed.
  # Gate 5a (new file pairing) and Gate 5b (mock density) still run — they're lightweight.
# ============================================================================
 2>&1 echo ""
 2>&1 echo "→ Gate 5: Tests & coverage..."
GATE_5_START=$(gate_start_ms)

if [ "$PROJECT_LANG" = "documentation-only" ]; then
  echo "✅ PASSED - Skipped (documentation project)."

else
  # ========================================================================
  # Gate 5a: Test-Source File Pairing Check
  # New .ts/.tsx files: BLOCK (no corresponding test → commit rejected)
  # Modified files (any lang): WARNING (backward compatible)
  # New non-TS files: WARNING (unchanged)
  # Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only, audit logged)
  # Grace period: .tdd-adoption.yaml gracePeriod: N (first N commits = WARNING only)
  # Checks coexistence, not temporal order. True "test-first" enforced by
  # Agent skills (Layer 1 + Layer 4).
  # ========================================================================

  # --- Pre-checks: grace period + escape valve ---
  TDD_GRACE=0
  if [ -f ".tdd-adoption.yaml" ]; then
    TDD_ENABLED=$(grep -E '^\s*enabled:' .tdd-adoption.yaml | awk '{print $2}' || echo "true")
    if [ "$TDD_ENABLED" != "false" ]; then
      TDD_GRACE=$(grep -E '^\s*gracePeriod:' .tdd-adoption.yaml | awk '{print $2}' || echo "0")
    fi
  fi

  TDD_BLOCK_DOWNGRADE=false
  if [ "$TDD_GRACE" -gt 0 ] 2>/dev/null; then
    TDD_BLOCK_DOWNGRADE=true
  fi

  SKIP_5A_BLOCK=false
  if [ "${SKIP_GATE_5A_BLOCK:-0}" = "1" ]; then
    CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
    if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
      REASON="${SKIP_GATE_5A_BLOCK_REASON:-not specified}"
      mkdir -p .xp-gate/reports
      # Escape special characters in REASON for valid JSON
      ESCAPED_REASON=$(printf '%s' "$REASON" | sed 's/\\/\\\\/g; s/"/\\"/g')
      ESCAPED_USER=$(printf '%s' "$(git config user.name)" | sed 's/\\/\\\\/g; s/"/\\"/g')
      echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"branch\":\"$CURRENT_BRANCH\",\"user\":\"$ESCAPED_USER\",\"reason\":\"$ESCAPED_REASON\",\"gate\":\"5a-block\"}" >> .xp-gate/reports/escape-valve-log.json
      echo "⚠️  ESCAPE VALVE: SKIP_GATE_5A_BLOCK=1 on branch $CURRENT_BRANCH (reason: $REASON)"
      echo "   ⚠️  Post-mortem issue required within 24h"
      SKIP_5A_BLOCK=true
    else
      echo "❌ ESCAPE VALVE BLOCKED: SKIP_GATE_5A_BLOCK not allowed on main/master branch"
      exit 1
    fi
  fi

  # --- BLOCK path: New .ts/.tsx files without corresponding tests ---
  NEW_TS_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '\.d\.ts$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v 'node_modules/' | grep -v '\.next/' | grep -v '\.nuxt/' | grep -v 'dist/' | grep -v 'build/' | grep -v '.turbo/' | grep -v '.cache/' | grep -Ev '(^|/)vitest\.config\.' | grep -Ev '(^|/)vite\.config\.' | grep -Ev '(^|/)jest\.config\.' | grep -Ev '(^|/)tsconfig\.' | grep -Ev '(^|/)eslint\.' | grep -Ev '(^|/)prettier\.' | grep -Ev '(^|/)tailwind\.' || true)

  # Strip PROJECT_SUBDIR prefix from new-TS-file paths when running in a subdirectory.
  # git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR"
  # the test-pairing [ -f ] checks below need subdirectory-relative paths.
  # (Mirrors the v0.14.31+ Gate 5b fix at lines 1603-1608; Gate 5a was missing it.)
  if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$NEW_TS_FILES" ]; then
    NEW_TS_FILES=$(echo "$NEW_TS_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
  fi

  if [ -n "$NEW_TS_FILES" ]; then
    BLOCKED_TS_FILES=0
    for src_file in $NEW_TS_FILES; do
      base="${src_file%.*}"
      ext="${src_file##*.}"
      filename="${base##*/}"
      dir="$(dirname "$src_file")"

      # Skip excluded files: index/barrel, types, interfaces, constants, declarations
      case "$filename" in
        index|types|interfaces|constants|__init__) continue ;;
      esac
      case "$ext" in
        d.ts|pyi) continue ;;
      esac
      # Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
      # Order matters: @no-test-required must be checked first — @no-test is a substring match
      if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
        if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
          continue
        fi
        # @no-test-required with short reason (< 10 chars) — fall through to BLOCK
      elif grep -q '@no-test' "$src_file" 2>/dev/null; then
        continue
      fi

      # Check common test file patterns for TypeScript
      TEST_FOUND=false
      patterns=(
        "${base}.test.${ext}"
        "${base}.spec.${ext}"
        "${dir}/__tests__/${filename}.test.${ext}"
        "${dir}/__tests__/${filename}.spec.${ext}"
        "tests/${filename}.test.${ext}"
        "tests/${filename}.spec.${ext}"
      )

      for pattern in "${patterns[@]}"; do
        if [[ "$pattern" == *'*'* ]]; then
          found=$(find . -path "*/${pattern}" -type f 2>/dev/null | head -1)
          if [ -n "$found" ]; then TEST_FOUND=true; break; fi
        elif [ -f "$pattern" ]; then
          TEST_FOUND=true
          break
        fi
      done

      if [ "$TEST_FOUND" = false ]; then
        if [ "$SKIP_5A_BLOCK" = true ] || [ "$TDD_BLOCK_DOWNGRADE" = true ]; then
          echo "⚠️  TEST PAIRING WARNING (downgraded): New TypeScript file without test: $src_file"
          echo "   Expected: ${patterns[0]}, ${patterns[1]:-...}"
          echo "   Or: Add '// @no-test-required: <reason>' if this file doesn't need tests"
        else
          echo "❌ BLOCKED: $src_file — New TypeScript source file without corresponding test"
          echo "   Expected: ${patterns[0]}, ${patterns[1]:-...}"
          echo "   Add '// @no-test-required: <reason>' if this file doesn't need tests"
          BLOCKED_TS_FILES=$((BLOCKED_TS_FILES + 1))
        fi
      fi
    done

    if [ "$BLOCKED_TS_FILES" -gt 0 ]; then
      echo ""
      echo "❌ $BLOCKED_TS_FILES new TypeScript file(s) without corresponding tests — commit BLOCKED"
      echo "   Add test files or '// @no-test-required: <reason>' annotation."
      echo "   Escape valve: SKIP_GATE_5A_BLOCK=1 (non-main/master only)"
      exit 1
    fi
  fi

  # --- WARNING path: Modified files (any lang) + New non-TS files ---
  WARNING_SOURCE_FILES=$(
    {
      git diff --cached --name-only --diff-filter=M | grep -E '\.(ts|tsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
      git diff --cached --name-only --diff-filter=A | grep -E '\.(py|go|java|kt|kts|cpp|cc|cxx|c|swift|dart|rb|rs)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__'
    } | sort -u || true
  )

  if [ -n "$WARNING_SOURCE_FILES" ]; then
    PAIRING_WARNINGS=0
    for src_file in $WARNING_SOURCE_FILES; do
      base="${src_file%.*}"
      ext="${src_file##*.}"
      filename="${base##*/}"
      dir="$(dirname "$src_file")"

      # Skip excluded files: index/barrel, types, interfaces, constants, declarations
      case "$filename" in
        index|types|interfaces|constants|__init__) continue ;;
      esac
      case "$ext" in
        d.ts|pyi) continue ;;
      esac
      # Skip files with @no-test-required (reason >= 10 chars) or legacy @no-test annotation
      # Order matters: @no-test-required must be checked first — @no-test is a substring match
      if grep -q '@no-test-required' "$src_file" 2>/dev/null; then
        if grep -qE '@no-test-required\s*:\s*.{10,}' "$src_file" 2>/dev/null; then
          continue
        fi
        # @no-test-required with short reason (< 10 chars) — fall through to BLOCK
      elif grep -q '@no-test' "$src_file" 2>/dev/null; then
        continue
      fi

      # Check common test file patterns (language-specific)
      TEST_FOUND=false
      case "$ext" in
        ts|tsx|js|jsx)
          patterns=(
            "${base}.test.${ext}"
            "${base}.spec.${ext}"
            "${dir}/__tests__/${filename}.test.${ext}"
            "${dir}/__tests__/${filename}.spec.${ext}"
            "tests/${filename}.test.${ext}"
            "tests/${filename}.spec.${ext}"
          )
          ;;
        py)
          patterns=(
            "tests/${filename}_test.py"
            "tests/test_${filename}.py"
            "${dir}/tests/test_${filename}.py"
            "${base}_test.py"
          )
          ;;
        go)
          patterns=(
            "${base}_test.go"
            "${dir}/${filename}_test.go"
          )
          ;;
        java|kt|kts)
          patterns=(
            "${base}Test.${ext}"
            "src/test/java/**/${filename}Test.${ext}"
            "src/test/kotlin/**/${filename}Test.${ext}"
          )
          ;;
        cpp|cc|cxx|c)
          patterns=(
            "${base}_test.${ext}"
            "${base}Test.${ext}"
            "${dir}/test_${filename}.${ext}"
          )
          ;;
        swift)
          patterns=(
            "${base}Tests.swift"
            "${dir}/${filename}Tests.swift"
          )
          ;;
        dart)
          patterns=("${base}_test.${ext}")
          ;;
        rb)
          patterns=(
            "${dir}/test_${filename}.rb"
            "${base}_test.rb"
          )
          ;;
        rs)
          # Rust tests are typically inline with #[cfg(test)]
          continue
          ;;
        *) continue ;;
      esac

      for pattern in "${patterns[@]}"; do
        if [[ "$pattern" == *'*'* ]]; then
          found=$(find . -path "*/${pattern}" -type f 2>/dev/null | head -1)
          if [ -n "$found" ]; then TEST_FOUND=true; break; fi
        elif [ -f "$pattern" ]; then
          TEST_FOUND=true
          break
        fi
      done

      if [ "$TEST_FOUND" = false ]; then
        echo "⚠️  TEST PAIRING WARNING: Source file without corresponding test: $src_file"
        echo "   Expected patterns for .${ext}: ${patterns[0]}, ${patterns[1]:-...}"
        echo "   Or: Add '// @no-test-required: <reason>' annotation if this file doesn't need tests"
        PAIRING_WARNINGS=$((PAIRING_WARNINGS + 1))
      fi
    done

    if [ "$PAIRING_WARNINGS" -gt 0 ]; then
      echo ""
      echo "⚠️  $PAIRING_WARNINGS source file(s) without corresponding tests"
      echo "   This is a WARNING — commit proceeds. TDD order enforced by Agent skills."
      echo "   To suppress for specific files, add '// @no-test-required: <reason>' at the top."
    fi
  fi

  # ========================================================================
  # Gate 5b: Mock Density ADVISORY Scan (pure bash — no npx tsx overhead)
  # 30% = ADVISORY, 50% = suggests @mock-justified. Does NOT block commit.
  # ========================================================================
  CHANGED_TEST_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(test|spec)\.(ts|tsx|js|jsx|py|go)$' || true)

  # Strip PROJECT_SUBDIR prefix from test file paths when running in subdirectory.
  # git diff returns repo-root relative paths, but after cd "$PROJECT_SUBDIR" (line 728)
  # vitest and [ -f ] checks need subdirectory-relative paths. (v0.14.31+ fix)
  if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_TEST_FILES" ]; then
    CHANGED_TEST_FILES=$(echo "$CHANGED_TEST_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
  fi

  if [ -n "$CHANGED_TEST_FILES" ]; then
    echo "Checking mock density in test files..."
    for test_file in $CHANGED_TEST_FILES; do
      if [ -f "$test_file" ]; then
        # Count mock keyword references (precise patterns only)
        MOCK_COUNT=0
        for kw in 'jest\.mock' 'vi\.mock' 'jest\.spyOn' 'vi\.spyOn' 'jest\.fn' 'vi\.fn' \
                  'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
                  'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
                  '\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
          # Use grep -o piped to wc -l for reliable single-number output (grep -c can emit
          # multi-line output with filenames under some grep versions, causing bash arithmetic
          # syntax errors). grep -o extracts each match on its own line, wc -l counts them.
          c=$(grep -o "$kw" "$test_file" 2>/dev/null | wc -l || true)
          c=${c//[^0-9]/}
          c=${c:-0}
          MOCK_COUNT=$((MOCK_COUNT + c))
        done

        # Count total non-empty, non-comment lines for density denominator
        TOTAL_LINES=$(grep -v '^\s*$' "$test_file" | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '^\s*#' | wc -l | awk '{print $1}')

        if [ "$TOTAL_LINES" -gt 0 ] 2>/dev/null; then
          MOCK_DENSITY=$(awk "BEGIN {printf \"%.1f\", ($MOCK_COUNT / $TOTAL_LINES) * 100}")
        else
          MOCK_DENSITY="0"
        fi

        THRESHOLD_30=$(awk "BEGIN {print ($MOCK_DENSITY > 30) ? 1 : 0}")
        THRESHOLD_50=$(awk "BEGIN {print ($MOCK_DENSITY > 50) ? 1 : 0}")

        # Check for @mock-justified annotation with reason text (min 10 chars)
        HAS_JUSTIFIED=$(grep -qE '@mock-justified\s*:\s*.{10,}' "$test_file" 2>/dev/null && echo "true" || echo "false")

        if [ "$THRESHOLD_50" = "1" ]; then
          if [ "$HAS_JUSTIFIED" = "false" ]; then
            echo "⚠️  MOCK DENSITY: $test_file — ${MOCK_DENSITY}% (exceeds 50%)"
            echo "   Consider: integration test with real collaborators"
            echo "   Or: Add '// @mock-justified: <reason>' (min 10 char explanation)"
          else
            echo "📝 MOCK DENSITY: $test_file — ${MOCK_DENSITY}% (justified)"
          fi
        elif [ "$THRESHOLD_30" = "1" ]; then
          echo "ℹ️  MOCK DENSITY ADVISORY: $test_file — ${MOCK_DENSITY}% (consider reducing mocks)"
        fi
      fi
    done
  fi

  # ============================================================================
  # Gate 5c: Semantic Annotation Check (v0.19.0+)
  # Validates @test REQ-XXX annotations against specification.yaml.
  #
  # Strategy: Reads pre-compiled req-ids.json (produced by xp-gate check-alignment).
  # If req-ids.json is absent or stale, degrades to SKIP.
  # If present: greps staged tests for @test REQ-XXX, cross-references against
  # the pre-compiled REQ ID list. BLOCKs if any REQ ID doesn't exist in spec.
  # If any REQ in spec has no annotated test, emits visible [FAIL] message
  # (exit 0, appears in hook summary) — full alignment check runs at CLI time.
  # ============================================================================
  REQ_IDS_FILE=".sprint-state/phase-outputs/req-ids.json"
  if [ -f "$REQ_IDS_FILE" ] && command -v jq >/dev/null 2>&1; then
    # Check staleness: req-ids.json must be newer than specification.yaml
    SPEC_FILE="specification.yaml"
    REQ_IDS_STALE=false
    if [ -f "$SPEC_FILE" ]; then
      if [ "$REQ_IDS_FILE" -ot "$SPEC_FILE" ]; then
        REQ_IDS_STALE=true
      fi
    fi

    if [ "$REQ_IDS_STALE" = true ]; then
      echo "⚠️  Gate 5c: req-ids.json is stale (specification.yaml updated since last check-alignment)"
      echo "   Run 'xp-gate check-alignment' to regenerate before next commit."
    else
      CHANGED_TEST_FILES_ANNOTATED=$(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true)
      if [ -n "$CHANGED_TEST_FILES_ANNOTATED" ]; then
        echo "  → Checking @test REQ-XXX annotations..."
        ANNOTATION_ERRORS=0
        for tf in $CHANGED_TEST_FILES_ANNOTATED; do
          if [ -f "$tf" ]; then
            # Extract @test REQ-XXX annotations from staged test file (word-bounded)
            ANNOTATED_REQS=$(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' | sort -u || true)
            for req_id in $ANNOTATED_REQS; do
              # Cross-reference against pre-compiled REQ ID list
              FOUND=$(jq -r --arg rid "$req_id" '.[] | select(. == $rid)' "$REQ_IDS_FILE" 2>/dev/null || true)
              if [ -z "$FOUND" ]; then
                echo "❌ Gate 5c: $tf — @test $req_id: REQ ID not found in specification.yaml"
                ANNOTATION_ERRORS=$((ANNOTATION_ERRORS + 1))
              fi
            done
          fi
        done

        if [ "$ANNOTATION_ERRORS" -gt 0 ]; then
          echo ""
          echo "❌ $ANNOTATION_ERRORS test annotation(s) reference non-existent REQ IDs — commit BLOCKED"
          echo "   Fix: Verify REQ IDs against specification.yaml, or run 'xp-gate check-alignment'"
          exit 1
        fi

        # Check reverse: any REQ in spec with no annotated test → visible FAIL (non-blocking)
        ALL_SPEC_REQS=$(jq -r '.[]' "$REQ_IDS_FILE" 2>/dev/null)
        ANNOTATED_ALL=""
        for tf in $(echo "$CHANGED_FILES" | grep -E '\.(test|spec)\.(ts|tsx|js|jsx)$' || true); do
          if [ -f "$tf" ]; then
            ANNOTATED_ALL="$ANNOTATED_ALL $(grep -oE '@test\s+REQ-[A-Z0-9-]+' "$tf" 2>/dev/null | sed 's/@test\s*//' || true)"
          fi
        done
        UNTESTED_COUNT=0
        for spec_req in $ALL_SPEC_REQS; do
          if ! echo "$ANNOTATED_ALL" | grep -qF "$spec_req"; then
            UNTESTED_COUNT=$((UNTESTED_COUNT + 1))
          fi
        done
        if [ "$UNTESTED_COUNT" -gt 0 ]; then
          echo "⚠️  [FAIL] Gate 5c: $UNTESTED_COUNT REQ(s) in specification.yaml have no annotated tests"
          echo "   This is non-blocking. Run 'xp-gate check-alignment' for full alignment report."
        fi

        echo "  ✅ @test annotation check complete."
      fi
    fi
  fi

  # ============================================================================
  # v0.14.3+ (Issue #312): Early skip when no code files changed.
  # If the commit only touches VERSION, CHANGELOG, docs, config, .gitignore,
  # or similar non-code files, skip the test run entirely.
  # Gate 5a (new file pairing) and 5b (mock density) are cheap and still run.
  # ============================================================================
  CODE_EXTENSIONS="\.(ts|tsx|js|jsx|py|go|java|kt|kts|cpp|cc|cxx|c|swift|m|mm|dart|sh|ps1)$"
  CHANGED_CODE_FILES=$(echo "$CHANGED_FILES" | grep -E "$CODE_EXTENSIONS" || true)
  if [ -z "$CHANGED_CODE_FILES" ]; then
    echo "✅ PASSED - No code files changed (only docs/config/VERSION), skipping test run."
    echo "   Changed files: $(echo "$CHANGED_FILES" | tr '\n' ' ')"
    TESTS_EXIT_CODE=0
    TESTS_SKIPPED=true
    GATE_5_END=$(gate_start_ms)
    GATE_5_ELAPSED=$((GATE_5_END - GATE_5_START))
    echo "   ⏱️  Gate 5: ${GATE_5_ELAPSED}ms (skipped — no code changes)"
  else

  # ============================================================================
  # Smart test selection (Issue #286): only run tests affected by changed files.
  # Reuses CHANGED_TEST_FILES already computed in Gate 5b (line 1244).
  # Full run fallback: >20 changed test files, or vitest not found.
  # ============================================================================
  echo "Running tests..."
  
  # Route to appropriate test runner via adapter
  for CURRENT_LANG in $PROJECT_LANGS; do
    ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
    if [ "${CURRENT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
    if source "$ADAPTER_FILE" 2>/dev/null; then
      # TypeScript: vitest with smart test selection
      if [ "$CURRENT_LANG" = "typescript" ] && { [ -f "package.json" ] || [ -f "$PROJECT_ROOT/package.json" ]; }; then
        if run_without_git_context npx vitest --version >/dev/null 2>&1; then
          # Count changed test files to decide strategy
          CHANGED_TEST_FILE_COUNT=$(echo "$CHANGED_TEST_FILES" | grep -c '.' || echo "0")
          
          # Also get changed source files (non-test) for related-test lookup
          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)
          # Strip PROJECT_SUBDIR prefix (same as CHANGED_TEST_FILES fix above)
          if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ -n "$CHANGED_SRC_FILES" ]; then
            CHANGED_SRC_FILES=$(echo "$CHANGED_SRC_FILES" | sed "s|^${PROJECT_SUBDIR}/||")
          fi
          CHANGED_SRC_COUNT=$(echo "$CHANGED_SRC_FILES" | grep -c '.' || echo "0")
          
          # Decide strategy
          if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
            # Too many test files changed → full run
            echo "Too many test files changed ($CHANGED_TEST_FILE_COUNT), running full test suite..."
            TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage 2>&1)
            TESTS_EXIT_CODE=$?
            echo "$TESTS_OUTPUT" | head -60
            if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
              echo ""
              echo "❌ BLOCKED - Tests FAILED"
              exit 1
            else
              echo "✅ PASSED - Full test suite passed (with coverage)."
            fi
          elif [ "$CHANGED_TEST_FILE_COUNT" -gt 0 ] 2>/dev/null; then
            # Run only changed test files with coverage
            SMART_TARGETS=$(echo "$CHANGED_TEST_FILES" | tr '\n' ' ')
            echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
            echo "   Target(s): $(echo "$CHANGED_TEST_FILES" | tr '\n' ' ')"
            TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage $SMART_TARGETS 2>&1)
            TESTS_EXIT_CODE=$?
            echo "$TESTS_OUTPUT" | head -60
            if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
              echo ""
              echo "❌ BLOCKED - Tests FAILED in changed test files"
              exit 1
            else
              echo "✅ PASSED - Changed test files passed (with coverage)."
            fi
          elif [ "$CHANGED_SRC_COUNT" -gt 0 ] 2>/dev/null; then
            # Source files changed but no test files → find related tests or run full
            echo "Source file(s) changed, finding related tests..."
            RELATED_TESTS=""
            for src_file in $CHANGED_SRC_FILES; do
              base="${src_file%.*}"
              filename="${base##*/}"
              dir="$(dirname "$src_file")"
              ext="${src_file##*.}"
              # Try common test file patterns
              for pattern in \
                "${base}.test.${ext}" \
                "${base}.spec.${ext}" \
                "${dir}/__tests__/${filename}.test.${ext}" \
                "${dir}/__tests__/${filename}.spec.${ext}" \
                "tests/${filename}.test.${ext}" \
                "tests/${filename}.spec.${ext}"; do
                if [ -f "$pattern" ]; then
                  RELATED_TESTS="${RELATED_TESTS}${pattern} "
                fi
              done
              # Glob fallback for __tests__ dir patterns
              if [ -d "${dir}/__tests__" ]; then
                found_glob=$(find "${dir}/__tests__" -maxdepth 1 -name "${filename}.test.*" -o -name "${filename}.spec.*" 2>/dev/null | tr '\n' ' ')
                if [ -n "$found_glob" ]; then
                  for f in $found_glob; do
                    case " $RELATED_TESTS " in *" $f "*) ;; *) RELATED_TESTS="${RELATED_TESTS}${f} " ;; esac
                  done
                fi
              fi
            done
            if [ -n "$RELATED_TESTS" ]; then
              echo "   Found related test(s): $(echo "$RELATED_TESTS" | tr '\n' ' ')"
              PARTIAL_TEST_RUN=true  # Mark as partial run for coverage check
              TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage $RELATED_TESTS 2>&1)
              TESTS_EXIT_CODE=$?
              echo "$TESTS_OUTPUT" | head -60
              if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
                echo ""
                echo "❌ BLOCKED - Tests FAILED in related test files"
                exit 1
              else
                echo "✅ PASSED - Related test files passed (with coverage)."
              fi
            else
              # No related tests found → full run is safest
              echo "   No related test files found, running full test suite..."
              TESTS_OUTPUT=$(run_without_git_context npx vitest run --coverage 2>&1)
              TESTS_EXIT_CODE=$?
              echo "$TESTS_OUTPUT" | head -60
              if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
                echo ""
                echo "❌ BLOCKED - Tests FAILED"
                exit 1
              else
                echo "✅ PASSED - Full test suite passed (with coverage)."
              fi
            fi
          else
            # No test files AND no source files changed → skip
            echo "✅ PASSED - No test or source files changed, skipping test run."
            TESTS_EXIT_CODE=0
            TESTS_SKIPPED=true
          fi
        else
          # Fallback: run_tests without coverage
          TESTS_OUTPUT=$(run_without_git_context run_tests 2>&1)
          TESTS_EXIT_CODE=$?
          echo "$TESTS_OUTPUT" | head -50
          if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
            echo ""
            echo "❌ BLOCKED - Tests FAILED"
            exit 1
          fi
        fi
      else
        # Non-TypeScript: standard adapter test flow
        # Skip for Python — tests already run with coverage in the coverage section below.
        # Running them here first without coverage doubles pre-commit time for no benefit.
        if [ "$CURRENT_LANG" = "python" ]; then
          echo "ℹ️  Python tests deferred to coverage section (single pass)."
        else
          TESTS_OUTPUT=$(run_without_git_context run_tests 2>&1)
          TESTS_EXIT_CODE=$?
          echo "$TESTS_OUTPUT" | head -50
          if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
            echo ""
            echo "❌ BLOCKED - Tests FAILED"
            exit 1
          else
            echo "✅ PASSED - Unit tests passed."
          fi
        fi
      fi
    else
      echo "✅ PASSED - Could not source ${CURRENT_LANG} adapter for tests, assuming none to run."
    fi
  else
    echo "✅ PASSED - No ${CURRENT_LANG} adapter found for tests, assuming none to run."
  fi
  done  # end for CURRENT_LANG

  fi  # End: if [ -n "$CHANGED_CODE_FILES" ] (Issue #312 early skip)
   
  # Next, run coverage check (skip if tests were skipped — Issue #312)
  if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
    echo "ℹ️  Test run was skipped — skipping coverage check."
  else
  echo "Running coverage check..."

  for CURRENT_LANG in $PROJECT_LANGS; do
  ADAPTER_FILE=$(resolve_adapter_path "$CURRENT_LANG")
  if [ "${CURRENT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
    if source "$ADAPTER_FILE" 2>/dev/null; then
      case "$CURRENT_LANG" in
        "typescript")
          # Coverage collected during the test section above.
          # Smart selection (Issue #286): only runs changed/related tests with --coverage,
          # producing coverage/coverage-summary.json for the tested subset.
          # Stage 2 enforcement reads global coverage from the subset run.
          # If no tests were run (TESTS_SKIPPED=true), skip coverage check entirely.
          if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
            echo "ℹ️  Test run was skipped — skipping coverage check."
            COV_EXIT=0
          elif [ -f "coverage/coverage-summary.json" ]; then
            echo "TypeScript coverage collected during test run."
            COV_EXIT=0
          else
            echo "ℹ️  No coverage data produced — test run was skipped (no TS changes)."
            COV_EXIT=0
          fi
          ;;
        "python")
          if command -v pytest &> /dev/null && command -v coverage &> /dev/null; then
            pytest --cov=. --cov-fail-under=80 --tb=short
            COV_EXIT=$?
          else
            echo "pytest/coverage not available, running coverage via adapter..."
            run_coverage_output=$(run_coverage 2>&1)
            COV_EXIT=$?
            echo "$run_coverage_output" | head -30
          fi
          ;;
        "go")
          if command -v go &> /dev/null; then
            go test -coverprofile=coverage.out ./... 2>/dev/null
            TOTAL_COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | grep "^total:" | awk '{print substr($3, 1, length($3)-1)}')
            if [ -n "$TOTAL_COVERAGE" ] && (( $(echo "$TOTAL_COVERAGE < 80" | bc -l 2>/dev/null || echo "0") )); then
              echo "❌ BLOCKED - Go coverage $TOTAL_COVERAGE% below 80% threshold"
              exit 1
            fi
            echo "Go coverage: $TOTAL_COVERAGE%"
            COV_EXIT=0
          fi
          ;;
        "shell")
          shell_cov_output=$(run_coverage 2>&1)
          COV_EXIT=$?
          echo "$shell_cov_output" | head -10
          echo "ℹ️  Shell coverage not typically measured"
          ;;
        *)
          default_cov_output=$(run_coverage 2>&1)
          COV_EXIT=$?
          echo "$default_cov_output" | head -30
          ;;
      esac
    else
      # Adapter source failed — warn and let Stage 2 attempt file-based enforcement
      echo "⚠️  Could not source ${CURRENT_LANG} adapter from ${ADAPTER_FILE}, will check coverage files directly..."
      COV_EXIT=0
    fi
  else
    # No adapter available — warn and let Stage 2 attempt file-based enforcement
    echo "⚠️  No adapter found for ${CURRENT_LANG} (searched: ${ADAPTER_DIR}/, ${ADAPTER_DIR}/adapters/, ${PROJECT_GITHOOKS}/adapters/, ${SCRIPT_DIR}/adapters/) — will check coverage files directly..."
    COV_EXIT=0
  fi
  done  # end for CURRENT_LANG (Stage 1 coverage)

  # ============================================================================
  # Stage 2: UNCONDITIONAL coverage percentage enforcement
  # Always attempt to parse coverage from available sources. If percentage
  # can be determined and is < 80% → BLOCK. If no data found → warn.
  #
  # NOTE: When only a subset of test files were run (smart selection, Issue #286),
  # coverage data reflects only the tested subset. In that case we warn but don't
  # block, because the full suite coverage may be ≥80% even though the subset
  # coverage is lower.
  # ============================================================================
  COVERAGE_ENFORCED=false

  for CURRENT_LANG in $PROJECT_LANGS; do
  case "$CURRENT_LANG" in
    "typescript")
      COVERAGE_ENFORCED=true
      if [ "${TESTS_SKIPPED:-false}" = "true" ]; then
        echo "✅ PASSED - Coverage check skipped (no test/source files changed)."
      elif [ -f "coverage/coverage-summary.json" ]; then
        COVERAGE_PERCENT=$(node -e "
          try {
            const fs = require('fs');
            const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
            console.log(Math.round(data.total.lines.pct));
          } catch(e) { console.log('parse_error'); }
        " 2>/dev/null)
        if [ "$COVERAGE_PERCENT" != "parse_error" ] && [ -n "$COVERAGE_PERCENT" ]; then
          if [ "$COVERAGE_PERCENT" -lt 80 ]; then
            # If only subset of tests ran (partial run or related tests), warn instead of block
            if [ "${PARTIAL_TEST_RUN:-false}" = "true" ] || { [ -n "${CHANGED_TEST_FILE_COUNT:-}" ] && [ "${CHANGED_TEST_FILE_COUNT:-0}" -gt 0 ] 2>/dev/null; }; then
              echo "⚠️  Partial coverage ${COVERAGE_PERCENT}% (subset test run, not full suite)."
              echo "    Run 'npx vitest run --coverage' for full coverage check."
            else
              echo "❌ BLOCKED - TypeScript coverage ${COVERAGE_PERCENT}% below 80% threshold"
              exit 1
            fi
          else
            echo "TypeScript coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
          fi
        else
          echo "⚠️  Could not parse TypeScript coverage from coverage/coverage-summary.json"
        fi
      else
        echo "⚠️  coverage/coverage-summary.json not found. If vitest --coverage was used, check vitest.config.ts coverageDirectory."
      fi
      ;;
    "python")
      COVERAGE_ENFORCED=true
      if command -v coverage &> /dev/null; then
        COVERAGE_PERCENT=""
        if [ -f ".coverage" ]; then
          COVERAGE_PERCENT=$(coverage report --format=total 2>/dev/null | grep -o '[0-9]*%' | head -1 | tr -d '%') || true
        fi
        if [ -z "$COVERAGE_PERCENT" ] && [ -f "coverage/.coverage" ]; then
          COVERAGE_PERCENT=$(coverage report --format=total -i coverage 2>/dev/null | grep -o '[0-9]*%' | head -1 | tr -d '%') || true
        fi
        if [ -n "$COVERAGE_PERCENT" ]; then
          if [ "$COVERAGE_PERCENT" -lt 80 ]; then
            echo "❌ BLOCKED - Python coverage ${COVERAGE_PERCENT}% below 80% threshold"
            exit 1
          fi
          echo "Python coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
        else
          echo "⚠️  Could not determine Python coverage percentage. Ensure pytest-cov or coverage.py data exists."
        fi
      else
        echo "⚠️  coverage.py not available, cannot enforce Python coverage threshold."
      fi
      ;;
    "go")
      # Go enforcement already handled inline in Stage 1 (lines 933-942)
      ;;
    "shell")
      # Shell coverage not typically measured
      ;;
    "dart"|"flutter")
      COVERAGE_ENFORCED=true
      if [ -f "coverage/lcov.info" ]; then
        COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT="0"
        if [ "$COVERAGE_PERCENT" != "0" ] && [ -n "$COVERAGE_PERCENT" ]; then
          if [ "$COVERAGE_PERCENT" -lt 80 ]; then
            echo "❌ BLOCKED - Flutter/Dart coverage ${COVERAGE_PERCENT}% below 80% threshold"
            exit 1
          fi
          echo "Flutter/Dart coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
        else
          echo "⚠️  Could not determine Flutter/Dart coverage percentage from lcov.info."
        fi
      else
        echo "⚠️  coverage/lcov.info not found. Ensure dart test --coverage was run."
      fi
      ;;
    "powershell")
      COVERAGE_ENFORCED=true
      if [ -f "coverage.xml" ]; then
        COVERAGE_RESULT=$(node -e "
          try {
            const fs = require('fs');
            const xml = fs.readFileSync('coverage.xml', 'utf8');
            const counters = [...xml.matchAll(/<counter\b([^>]*)\/?\s*>/gi)]
              .map((match) => Object.fromEntries(
                [...match[1].matchAll(/([\w:-]+)\s*=\s*(['\"])(.*?)\2/g)]
                  .map((attribute) => [attribute[1].toUpperCase(), attribute[3]])
              ))
              .filter((counter) => counter.TYPE === 'LINE');
            const reportCounter = counters.at(-1);
            const missed = Number(reportCounter?.MISSED);
            const covered = Number(reportCounter?.COVERED);
            const total = missed + covered;
            if (!Number.isFinite(missed) || !Number.isFinite(covered) || missed < 0 || covered < 0 || total <= 0) {
              throw new Error('invalid LINE counter');
            }
            const percentage = (covered / total) * 100;
            console.log((percentage < 80 ? 'below' : 'pass') + '|' + Math.round(percentage));
          } catch (error) { console.log('parse_error'); }
        " 2>/dev/null) || COVERAGE_RESULT="parse_error"
        if [ "$COVERAGE_RESULT" != "parse_error" ] && [ -n "$COVERAGE_RESULT" ]; then
          COVERAGE_STATUS=${COVERAGE_RESULT%%|*}
          COVERAGE_PERCENT=${COVERAGE_RESULT#*|}
          if [ "$COVERAGE_STATUS" = "below" ]; then
            echo "❌ BLOCKED - PowerShell coverage ${COVERAGE_PERCENT}% below 80% threshold"
            exit 1
          fi
          echo "PowerShell coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
        else
          echo "⚠️  Could not parse PowerShell coverage from coverage.xml"
        fi
      else
        echo "⚠️  coverage.xml not found. Ensure Pester coverage was run."
      fi
      ;;
    *)
      # Default: attempt to parse coverage from common formats regardless of language
      COVERAGE_ENFORCED=true
      COVERAGE_PERCENT=""
      if [ -f "coverage/coverage-summary.json" ]; then
        COVERAGE_PERCENT=$(node -e "
          try {
            const fs = require('fs');
            const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
            console.log(Math.round(data.total.lines.pct));
          } catch(e) { console.log('parse_error'); }
        " 2>/dev/null) || COVERAGE_PERCENT=""
      elif [ -f "coverage/lcov.info" ]; then
        if command -v parse_lcov_coverage &> /dev/null; then
          COVERAGE_PERCENT=$(parse_lcov_coverage "coverage/lcov.info" 2>/dev/null) || COVERAGE_PERCENT=""
        else
          COVERAGE_PERCENT=$(grep -oP 'SF:.*' coverage/lcov.info 2>/dev/null | wc -l || echo "0")
          # Simple fallback: check lcov for coverage percentage
          COVERAGE_PERCENT=$(grep -oP 'coverage: \K[0-9]+%' coverage/lcov.info 2>/dev/null | head -1 | tr -d '%') || COVERAGE_PERCENT=""
        fi
      fi
      if [ -n "$COVERAGE_PERCENT" ] && [ "$COVERAGE_PERCENT" != "parse_error" ] && [ "$COVERAGE_PERCENT" -gt 0 ]; then
        if [ "$COVERAGE_PERCENT" -lt 80 ]; then
          echo "❌ BLOCKED - ${CURRENT_LANG} coverage ${COVERAGE_PERCENT}% below 80% threshold"
          exit 1
        fi
        echo "${CURRENT_LANG} coverage: ${COVERAGE_PERCENT}% ✅ (≥ 80%)"
      else
        echo "⚠️  Could not determine ${CURRENT_LANG} coverage percentage. No standard coverage report files found."
      fi
      ;;
  esac

  if [ "$COV_EXIT" -ne 0 ]; then
    echo ""
    echo "❌ BLOCKED - Coverage check FAILED"
    exit 1
  fi
  if [ "$COVERAGE_ENFORCED" = false ]; then
    echo "ℹ️  Coverage enforcement not applicable for ${CURRENT_LANG}"
  fi
  echo "✅ PASSED - Coverage check completed."
  done  # end for CURRENT_LANG (Stage 2 coverage)

  fi  # End: if [ "${TESTS_SKIPPED:-false}" != "true" ] (Issue #312 coverage skip)

  # ============================================================================
  # Stage 3: Per-file coverage check for NEW files (Issue #103)
  # New files must have ≥80% coverage individually.
  # ============================================================================
  if has_project_lang "typescript" && [ -f "coverage/coverage-summary.json" ]; then
    NEW_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=A | grep -E '\.(ts|tsx)$' | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__snapshots__' | grep -v '\.d\.ts$' || true)
    if [ -n "$NEW_SOURCE_FILES" ]; then
      echo ""
      echo "   └─ Per-file coverage check for new files:"
      for new_file in $NEW_SOURCE_FILES; do
        # Normalize path for coverage-summary.json lookup
        NORMALIZED_PATH=$(echo "$new_file" | sed 's|^\./||')
        FILE_COV=$(node -e "
          try {
            const fs = require('fs');
            const data = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
            const entry = data['$NORMALIZED_PATH'];
            if (entry && entry.lines) {
              console.log(Math.round(entry.lines.pct));
            } else {
              console.log('no_data');
            }
          } catch(e) { console.log('parse_error'); }
        " 2>/dev/null)
        if [ "$FILE_COV" = "no_data" ] || [ "$FILE_COV" = "parse_error" ]; then
          echo "     ⚠️  $new_file — no coverage data (file may be excluded or not exercised)"
        elif [ "$FILE_COV" -lt 80 ]; then
          echo "     ❌ $new_file — ${FILE_COV}% coverage (below 80% threshold)"
          NEW_FILE_COV_FAIL=true
        else
          echo "     ✅ $new_file — ${FILE_COV}% coverage"
        fi
      done
      if [ "${NEW_FILE_COV_FAIL:-false}" = "true" ]; then
        echo ""
        echo "❌ BLOCKED - New file(s) below 80% coverage threshold"
        echo "Add tests to cover the new files before committing."
        exit 1
      fi
    fi
  fi
fi
GATE_5_STATUS="PASS"
record_gate_audit "gate-5" "tests-coverage" "$GATE_5_STATUS" "0" "$GATE_5_START"

# ============================================================================
# GATE 6: Architecture & Tech Debt (Combines Old Gate 8 - Boy Scout and Gate 9 - Architecture)
# ============================================================================
 2>&1 echo ""
 2>&1 echo "→ Gate 6: Architecture & tech debt..."
GATE_6_START=$(gate_start_ms)

# First Part: Architecture Validation (previously Gate 9)
2>&1 echo "   └─ Architecture validation:"

if [ "$PROJECT_LANG" = "documentation-only" ]; then
  echo "     ✅ Skipped (documentation project)."

elif has_project_lang "powershell" 2>/dev/null || [ "$PROJECT_LANG" = "powershell" ]; then
  echo "     ℹ️  No PowerShell architecture tooling available"
     echo "     ⏭️  SKIPPED - Architecture validation (no tool for PowerShell)"

else
  run_archlint() (
    cd "$PROJECT_ROOT" || exit 1
    if [ "$ARCHLINT_KIND" = "npx" ]; then
      npx @archlinter/cli "$@"
    else
      "$ARCHLINT_BIN" "$@"
    fi
  )

  # Check if architecture config exists (use $PROJECT_ROOT — hook may have cd'd to a subdir via ARCH-03)
  if [ -f "$PROJECT_ROOT/architecture.yaml" ] || [ -f "$PROJECT_ROOT/.architecturerc" ] || [ -f "$PROJECT_ROOT/.archlint.yaml" ]; then
    # Different architecture tools per language
    for CURRENT_LANG in $PROJECT_LANGS; do
    case "$CURRENT_LANG" in
      "typescript")
        # Try local npx install first (devDependencies), fall back to global
        if [ -x "$PROJECT_ROOT/node_modules/.bin/archlint" ]; then
          ARCHLINT_KIND="binary"
          ARCHLINT_BIN="$PROJECT_ROOT/node_modules/.bin/archlint"
        elif command -v archlint >/dev/null 2>&1; then
          ARCHLINT_KIND="binary"
          ARCHLINT_BIN="$(command -v archlint)"
        elif (cd "$PROJECT_ROOT" && npx @archlinter/cli --version) >/dev/null 2>&1; then
          ARCHLINT_KIND="npx"
        else
          echo "     ❌ BLOCKED - archlint not installed"
          echo "     Install with: npm install --save-dev @archlinter/cli"
          exit 1
        fi
         echo "     Running archlint for TypeScript..."
          # Use diff mode with baseline for ratchet (block only new violations)
          # Fall back to raw scan if no baseline exists (new project, zero-tolerance)
          if [ -f "$PROJECT_ROOT/.architecture-baseline.json" ]; then
            ARCHLINT_OUTPUT=$(run_archlint diff .architecture-baseline.json --fail-on high 2>&1)
          else
            ARCHLINT_OUTPUT=$(run_archlint scan . -f table --quiet 2>&1)
          fi
         ARCHLINT_EXIT=$?
         echo "$ARCHLINT_OUTPUT" | tail -30
         if [ "$ARCHLINT_EXIT" -ne 0 ]; then
           echo ""
           echo "❌ BLOCKED - Architecture violations detected"
             if [ -f "$PROJECT_ROOT/.architecture-baseline.json" ]; then
             echo "New architecture violations found (baseline ratchet mode)."
             echo "Fix the new violations above, or update the baseline with: npx @archlinter/cli snapshot -o .architecture-baseline.json"
           else
             echo "Fix the architecture violations above before committing."
             echo "Or generate a baseline for gradual adoption: npx @archlinter/cli snapshot -o .architecture-baseline.json"
           fi
           exit 1
         fi
           if [ -f "$PROJECT_ROOT/.architecture-baseline.json" ]; then
           echo "     ✅ TypeScript architecture validation completed (baseline ratchet mode)."
         else
           echo "     ✅ TypeScript architecture validation completed (zero-tolerance mode). no baseline found."
         fi
         ;;
      "python")
        if [ -f ".import-linter.yml" ] || [ -f "import_linter_config.yml" ]; then
          if require_tool "lint-imports" "Gate 6" "pip install import-linter"; then
            echo "     Running import-linter for Python..."
            lint-imports 2>&1 | head -20
            echo "     ✅ Python architecture validation completed."
          else
            echo "     ❌ BLOCKED - import-linter not installed"
            echo "     Install with: pip install import-linter"
            exit 1
          fi
        else
          echo "     ℹ️  No .import-linter.yml found - skipping Python architecture"
          echo "     ⏭️  SKIPPED - Python architecture validation (no config)"
        fi
        ;;
      "go")
        if [ -f "arch-go.yaml" ] || [ -f "arch-go.yml" ]; then
          if require_tool "arch-go" "Gate 6" "go install github.com/arch-go/arch-go@latest"; then
            echo "     Running arch-go for Go..."
            arch-go check 2>&1 | head -20
            echo "     ✅ Go architecture validation completed."
          else
            echo "     ❌ BLOCKED - arch-go not installed"
            echo "     Install with: go install github.com/arch-go/arch-go@latest"
            exit 1
          fi
        else
          echo "     ℹ️  No arch-go.yaml found - skipping Go architecture"
          echo "     ⏭️  SKIPPED - Go architecture validation (no config)"
        fi
        ;;
      "java")
        if [ -f "src/test/java/architecture" ] || [ -d "src/test/java/architecture" ]; then
          echo "     Running Java architecture tests..."
          if [ -f "pom.xml" ]; then
            mvn test -Dtest=architecture.* 2>&1 | head -20 || mvn test -Dtest=**Architecture** 2>&1 | head -20 || echo "⚠️  Java architecture tests completed"
          elif [ -f "build.gradle" ]; then
            if [ -f "./gradlew" ]; then
              ./gradlew test --tests "*Architecture*" 2>&1 | head -20 || echo "⚠️  Java architecture tests completed"
            fi
          fi
          echo "     ✅ Java architecture validation completed."
        else
          echo "     ℹ️  No architecture test files found - skipping"
          echo "     ⏭️  SKIPPED - Architecture validation (no tests)"
        fi
        ;;
      *)
        echo "     ℹ️  Architecture validation not configured for $CURRENT_LANG"
        echo "     ⏭️  SKIPPED - Architecture validation (not configured for $CURRENT_LANG)"
        ;;
    esac
    done  # end for CURRENT_LANG (architecture)
  else
    # Architecture config is REQUIRED — BLOCK if missing
    if [ -f "$PROJECT_ROOT/.architecture-skip" ]; then
      echo "     ⚠️  Architecture config missing but .architecture-skip present — allowed to skip"
      echo "     ⏭️  SKIPPED - Architecture validation (.architecture-skip exemption)"
    else
      echo ""
      echo "❌ ARCHITECTURE CONFIG MISSING - COMMIT BLOCKED"
      echo "   Required configuration file (.archlint.yaml / architecture.yaml) is NOT found."
      echo "   Architecture Quality Gate requires explicit architecture constraints."
      echo ""
  echo "   Quick fix: create .archlint.yaml with: npx archlint init --no-interactive"
      echo "   Or architecture.yaml for custom Clean Architecture layer definitions:"
      echo "     layers:"
      echo "       - name: api"
      echo "         paths: [\"src/api/**\"]"
      echo "       - name: domain"
      echo "         paths: [\"src/domain/**\"]"
      echo "         allowed_imports: [domain]"
      echo "       - name: infrastructure"
      echo "         paths: [\"src/infrastructure/**\"]"
      echo "         allowed_imports: [domain, infrastructure]"
      echo ""
      echo "   After creating architecture.yaml, retry the commit."
      echo "   (To skip with warning, create .architecture-skip file)"
      exit 1
    fi
  fi
fi

# Second Part: Boy Scout Rule (previously Gate 8) - Unified Enforcement
2>&1 echo "   └─ Boy Scout Rule enforcement:"

if [ "$PROJECT_LANG" = "documentation-only" ]; then
  echo "     ✅ Skipped (documentation project)."
  
else
  # Check for Boy Scout script in installed modules first, then project src/
  BOY_SCOUT_SCRIPT=""
  if [ -f ".xp-gate/modules/principles/boy-scout.ts" ]; then
    BOY_SCOUT_SCRIPT=".xp-gate/modules/principles/boy-scout.ts"
  elif [ -f "src/principles/boy-scout.ts" ]; then
    BOY_SCOUT_SCRIPT="src/principles/boy-scout.ts"
  fi
  
  if [ -n "$BOY_SCOUT_SCRIPT" ]; then
    echo "     Checking Boy Scout Rule compliance..."
    
    # Separate new files and modified files
    NEW_FILES=$(git diff --cached --name-only --diff-filter=A | tr '\n' ' ')
    MODIFIED_FILES=$(git diff --cached --name-only --diff-filter=M | tr '\n' ' ') 
    
    if [ -z "$NEW_FILES" ] && [ -z "$MODIFIED_FILES" ]; then
      echo "     ✅ No new or modified files to check."
    else
      # Run Boy Scout Rule enforcement
      if command -v npx &> /dev/null; then
        # Skip if no files to check
        if [ -z "$(echo "$NEW_FILES" | tr -d ' ')" ] && [ -z "$(echo "$MODIFIED_FILES" | tr -d ' ')" ]; then
          echo "     ✅ No new or modified source files for Boy Scout check."
        else
          BOY_SCOUT_OUTPUT=$(npx tsx $BOY_SCOUT_SCRIPT \
            --new-files "$(echo "$NEW_FILES" | tr ' ' ',')" \
            --modified-files "$(echo "$MODIFIED_FILES" | tr ' ' ',')" \
            --baseline ".warnings-baseline.json" 2>&1)
        
        BOY_SCOUT_EXIT=$?
        
        if [ "$BOY_SCOUT_EXIT" -ne 0 ]; then
          echo "$BOY_SCOUT_OUTPUT"
          echo ""
          echo "❌ BLOCKED - Boy Scout Rule violation"
          echo "Requirements:"
          echo "  - NEW files: must have zero warnings"
          echo "  - MODIFIED files: cannot increase warnings from baseline"
          echo "  - Files with ≤5 warnings: must clear to zero"
          exit 1
        fi
        
        # Check output for violations
        VIOLATION_COUNT=$(echo "$BOY_SCOUT_OUTPUT" | grep -c '"enforcement": "BLOCK"' 2>/dev/null || true)
        VIOLATION_COUNT=${VIOLATION_COUNT:-0}
        if [ "$VIOLATION_COUNT" -gt 0 ]; then
          echo "$BOY_SCOUT_OUTPUT"
          echo ""
          echo "❌ BLOCKED - Boy Scout Rule violations detected ($VIOLATION_COUNT)"
          exit 1
        fi
        
        echo "     ✅ PASSED - Boy Scout Rule compliance."
        fi
      else
        echo "     ℹ️  npx not available - skipping Boy Scout Rule"
        echo "     ⏭️  SKIPPED - Boy Scout Rule (Node.js/npx not available)"
      fi
    fi
  else
    echo "     ℹ️  Boy scout rule not available in project - skipping"
    echo "     ⏭️  SKIPPED - Boy Scout Rule (not available in project)"
  fi
fi
GATE_6_STATUS="PASS"
record_gate_audit "gate-6" "architecture-boy-scout" "$GATE_6_STATUS" "${BS_BLOCKED:-0}" "$GATE_6_START"

# ============================================================================

# ============================================================================
# GATE 7: IaC Security Scanning (Terraform, Kubernetes, Docker)
# Extracted to gate-7.sh for maintainability
# v0.14.3+ (Issue #312): Skip if no IaC files changed
# ============================================================================
if any_changed_files_match "\.tf$" "\.tfvars$" "Dockerfile" "\.kube/" "\.ya?ml$"; then
source "$GATE_DIR/gate-7.sh"
else
  echo "✅ PASSED - No IaC files changed, skipping Gate 7."
fi


# GATE 8: Secret Scanning (gitleaks)
# Extracted to gate-8.sh for maintainability
# ============================================================================
source "$GATE_DIR/gate-8.sh"

# Switch back to original directory if we were in a subdirectory
if [ -n "$ORIGINAL_DIR" ]; then
  cd "$ORIGINAL_DIR" 
fi

# ============================================================================
# GATE 9: Build Integrity Check (TypeScript compilation + package + imports)
# ============================================================================
2>&1 echo ""
2>&1 echo "→ Gate 9: Build integrity... "
GATE_9_START=$(gate_start_ms)
GATE_9_STATUS="SKIP"

if [ -f "package.json" ] && [ -f "tsconfig.json" ]; then
  GATE_9_SCRIPT=""
  if [ -f "$PROJECT_ROOT/src/build-integrity/gate-10.ts" ]; then
    GATE_9_SCRIPT="$PROJECT_ROOT/src/build-integrity/gate-10.ts"
  elif [ -f ".xp-gate/modules/build-integrity/gate-10.ts" ]; then
    GATE_9_SCRIPT=".xp-gate/modules/build-integrity/gate-10.ts"
  elif [ -f "$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts" ]; then
    GATE_9_SCRIPT="$HOME/.config/xp-gate/modules/build-integrity/gate-10.ts"
  fi

  if [ -n "$GATE_9_SCRIPT" ]; then
    CHANGED_FILES_CSV=$(echo "$CHANGED_FILES" | tr ' ' ',')
    GATE_9_OUTPUT=$(mktemp)
    if timeout 120s npx tsx "$GATE_9_SCRIPT" --changed-files "$CHANGED_FILES_CSV" --project-root "$PROJECT_ROOT" --timeout 115000 > "$GATE_9_OUTPUT" 2>&1; then
      GATE_9_STATUS="PASS"
      echo "     ✅ Build integrity check passed."
    else
      GATE_9_EXIT=$?
      if [ $GATE_9_EXIT -eq 124 ]; then
        GATE_9_STATUS="SKIP"
        echo "     ⏱️  Build integrity check timed out (120s). SKIPPED."
      else
        GATE_9_STATUS="BLOCK"
        cat "$GATE_9_OUTPUT"
        echo ""
        echo "❌ BLOCKED - Gate 9: Build integrity check failed"
        echo "Fix type errors, broken imports, or package manifest issues before committing."
        rm -f "$GATE_9_OUTPUT"
        exit 1
      fi
    fi
    rm -f "$GATE_9_OUTPUT"
  else
    echo "     ℹ️  Build integrity script not found - skipping"
    echo "     ⏭️  SKIPPED - Build integrity (gate-10.ts not available)"
  fi
else
  echo "     ℹ️  Not a TypeScript project - skipping build integrity check"
  echo "     ⏭️  SKIPPED - Build integrity (not a TypeScript project)"
fi

GATE_9_END=$(date +%s)
GATE_9_DURATION=$((GATE_9_END - GATE_9_START))
echo "✅ Gate 9 completed in ${GATE_9_DURATION}s"
record_gate_audit "gate-9" "build-integrity" "$GATE_9_STATUS" "0" "$GATE_9_START"
echo ""
# ============================================================================

# ============================================================================
# GATE 10: Semgrep SAST Security Scan
# Extracted to gate-10.sh for maintainability
# v0.14.3+ (Issue #312): Skip if no code files changed
# v0.18.5 (Issue #397): Fix gate status variable collision — SAST now sets GATE_10_STATUS
# ============================================================================
if any_changed_files_match "\.(ts|tsx|js|jsx|py|go|java|kt|cpp|c|swift|m|dart|sh)$"; then
  if [ -f "$GATE_DIR/gate-10.sh" ]; then
    source "$GATE_DIR/gate-10.sh"
  else
    echo "⏭️  SKIPPED - SAST (gate-10.sh not found)"
    GATE_10_STATUS="SKIP"
  fi
else
  echo "✅ PASSED - No code files changed, skipping SAST scan."
  GATE_10_STATUS="PASS"
fi
# ============================================================================

# ============================================================================
# GATE 11: Sprint Flow Enforcement
# Validates sprint state consistency (delphi-review APPROVED before BUILD)
# Uses sprint-gate.sh for standalone validation logic
# ============================================================================
GATE_11_START=$(date +%s)
GATE_11_STATUS="PASS"

SPRINT_GATE_SCRIPT=""
if [ -f "$GATE_DIR/sprint-gate.sh" ]; then
  SPRINT_GATE_SCRIPT="$GATE_DIR/sprint-gate.sh"
elif [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/sprint-gate.sh" ]; then
  SPRINT_GATE_SCRIPT="$(git rev-parse --show-toplevel)/githooks/sprint-gate.sh"
fi

if [ -n "$SPRINT_GATE_SCRIPT" ]; then
  if ! bash "$SPRINT_GATE_SCRIPT" --pre-commit; then
    GATE_11_STATUS="BLOCK"
    echo "❌ BLOCKED - Gate 11: Sprint Flow Enforcement"
    record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "1" "$GATE_11_START"
    # Sprint gate failure is a hard block — exit immediately
    exit 1
  fi
else
  echo "⏭️  SKIPPED - Gate 11: Sprint Flow (sprint-gate.sh not found)"
  GATE_11_STATUS="SKIP"
fi

GATE_11_END=$(date +%s)
GATE_11_DURATION=$((GATE_11_END - GATE_11_START))
echo "✅ Gate 11 completed in ${GATE_11_DURATION}s"
record_gate_audit "gate-11" "sprint-flow" "$GATE_11_STATUS" "0" "$GATE_11_START"
echo ""
# ============================================================================

# ============================================================================
# GATE 12: File Hygiene Check
# Detects trailing whitespace, missing EOF newlines, merge conflict markers,
# and oversized files in staged changes.
# v0.14.15+ (Issue #350, #351)
# ============================================================================
if [ -f "$GATE_DIR/gate-12-file-hygiene.sh" ]; then
  source "$GATE_DIR/gate-12-file-hygiene.sh"
elif [ -f "$(git rev-parse --show-toplevel 2>/dev/null)/githooks/gate-12-file-hygiene.sh" ]; then
  source "$(git rev-parse --show-toplevel)/githooks/gate-12-file-hygiene.sh"
else
  echo "⏭️  SKIPPED - Gate 12: File Hygiene (gate-12-file-hygiene.sh not found)"
fi
# ============================================================================

generate_quality_report() {
  local HISTORY_FILE=".quality-history.jsonl"
  local COMMIT_HASH
  local TIMESTAMP
  local BRANCH
  local PASSED_COUNT=0
  local TOTAL_GATES=12

  COMMIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
  BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

  for gate in 1 2 3 4 5 6 7 8 9 10 11 12; do
    local status_var="GATE_${gate}_STATUS"
    if [ "${!status_var}" = "PASS" ]; then
      PASSED_COUNT=$((PASSED_COUNT + 1))
    fi
  done

  local SCORE=0
  if command -v bc >/dev/null 2>&1; then
    SCORE=$(echo "scale=1; ($PASSED_COUNT / $TOTAL_GATES) * 10" | bc)
  else
    SCORE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_COUNT / $TOTAL_GATES) * 10}")
  fi

  local BS_PASSED=0
  local BS_BLOCKED=0
  if [ -n "$BOY_SCOUT_OUTPUT" ]; then
    BS_PASSED=$(echo "$BOY_SCOUT_OUTPUT" | grep -o '"passedFiles": [0-9]*' | grep -o '[0-9]*' || echo "0")
    BS_BLOCKED=$(echo "$BOY_SCOUT_OUTPUT" | grep -o '"blockedFiles": [0-9]*' | grep -o '[0-9]*' || echo "0")
  fi

  local COV_PCT="${COVERAGE_PERCENT:-N/A}"

  # ── Build failed-tests list from TESTS_OUTPUT (Gate 5) ──────────────────────────────
  local FAILED_TESTS_JSON="[]"
  if [ -n "$TESTS_OUTPUT" ] && echo "$TESTS_OUTPUT" | grep -qi "fail\|error\|FAILED"; then
    # Extract failure lines: "FAIL src/foo.test.ts" or "  ✗ test name"
    local FAIL_LINES
    FAIL_LINES=$(echo "$TESTS_OUTPUT" | grep -E "^\s*(FAIL|✗|failed|AssertionError)" | head -10 | \
      sed 's/"/\\"/g' | awk '{ printf "%s%s%s", sep, "\"", $0; sep="," }')
    if [ -n "$FAIL_LINES" ]; then
      FAILED_TESTS_JSON="[$FAIL_LINES]"
    fi
  fi

  # ── Branch-level quality status file ─────────────────────────────────────────────
  local STATUS_DIR=".xp-gate/quality-status"
  local REPORT_FILE="${STATUS_DIR}/${BRANCH}.json"
  mkdir -p "$(dirname "$REPORT_FILE")"

  cat > "$REPORT_FILE" << ENDJSON
{
  "reportVersion": "2.0",
  "generatedAt": "$TIMESTAMP",
  "branch": "$BRANCH",
  "commit": "$COMMIT_HASH",
  "language": "${PROJECT_LANGS:-${PROJECT_LANG:-unknown}}",
  "overall": {
    "gatesPassed": $PASSED_COUNT,
    "gatesTotal": $TOTAL_GATES,
    "score": $SCORE,
    "verdict": "$([ "$PASSED_COUNT" -eq "$TOTAL_GATES" ] && echo "PASS" || echo "PARTIAL")"
  },
  "gates": {
    "gate1_static_analysis": {
      "name": "Code Quality (Static + Lint + Shell)",
      "status": "${GATE_1_STATUS:-PASS}",
      "tool": "${GATE_1_TOOL:-auto}"
    },
    "gate2_dup_code": {
      "name": "Duplicate Code",
      "status": "${GATE_2_STATUS:-PASS}",
      "metric": "similarity <= 5%"
    },
    "gate3_complexity": {
      "name": "Cyclomatic Complexity",
      "status": "${GATE_3_STATUS:-PASS}",
      "threshold": "${CCN_THRESHOLD:-5}",
      "blockThreshold": "${CCN_THRESHOLD:-5}",
      "warnings": ${CC_WARNINGS:-0}
    },
    "gate4_principles": {
      "name": "Clean Code + SOLID",
      "status": "${GATE_4_STATUS:-PASS}",
      "warnings": ${WARNING_COUNT:-0}
    },
    "gate5_tests": {
      "name": "Tests + Coverage",
      "status": "${GATE_5_STATUS:-PASS}",
      "thresholds": { "lines": 80, "functions": 80, "branches": 70, "statements": 80 },
      "actual": { "coverage": "${COV_PCT}" },
      "failedTests": $FAILED_TESTS_JSON
    },
    "gate6_arch_boyscout": {
      "name": "Architecture + Boy Scout Rule",
      "status": "${GATE_6_STATUS:-PASS}",
      "boyScoutPassed": $BS_PASSED,
      "boyScoutBlocked": $BS_BLOCKED
    },
    "gate7_iac_security": {
      "name": "IaC Security Scanning",
      "status": "${GATE_7_STATUS:-PASS}",
      "tools": "checkov, hadolint, kube-score, tflint"
    },
    "gate8_secret_scanning": {
      "name": "Secret Scanning",
      "status": "${GATE_8_STATUS:-PASS}",
      "tool": "gitleaks"
    },
    "gate9_build_integrity": {
      "name": "Build Integrity",
      "status": "${GATE_9_STATUS:-PASS}",
      "tool": "tsc + npm pack + import check"
    },
    "gate10_sast": {
      "name": "SAST Security Scan",
      "status": "${GATE_10_STATUS:-PASS}",
      "tool": "semgrep"
    },
    "gate11_sprint_flow": {
      "name": "Sprint Flow Enforcement",
      "status": "${GATE_11_STATUS:-PASS}",
      "tool": "sprint-gate.sh"
    },
    "gate12_file_hygiene": {
      "name": "File Hygiene Check",
      "status": "${GATE_12_STATUS:-WARN}",
      "tool": "gate-12-file-hygiene.sh"
    }
  }
}
ENDJSON

  # ── Persistent per-run report (Issue #101) ────────────────────────────────────────
  local REPORTS_DIR=".xp-gate/reports/pre-commit"
  local REPORT_TS
  REPORT_TS=$(date -u +"%Y-%m-%d-%H%M%S")
  local PERSISTED_REPORT_FILE="${REPORTS_DIR}/${REPORT_TS}.json"
  mkdir -p "$REPORTS_DIR"
  _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"

  # ── History append (unchanged — append-only for trend) ────────────────────────────
  local GATES_JSON="{"
  GATES_JSON="${GATES_JSON}\"gate1\":{\"status\":\"${GATE_1_STATUS:-PASS}\",\"name\":\"Code Quality\"},"
  GATES_JSON="${GATES_JSON}\"gate2\":{\"status\":\"${GATE_2_STATUS:-PASS}\",\"name\":\"Duplicate Code\"},"
  GATES_JSON="${GATES_JSON}\"gate3\":{\"status\":\"${GATE_3_STATUS:-PASS}\",\"name\":\"Complexity\",\"warnings\":${CC_WARNINGS:-0}},"
  GATES_JSON="${GATES_JSON}\"gate4\":{\"status\":\"${GATE_4_STATUS:-PASS}\",\"name\":\"Principles\",\"warnings\":${WARNING_COUNT:-0}},"
  GATES_JSON="${GATES_JSON}\"gate5\":{\"status\":\"${GATE_5_STATUS:-PASS}\",\"name\":\"Tests+Coverage\",\"coverage\":\"${COV_PCT}\"},"
  GATES_JSON="${GATES_JSON}\"gate6\":{\"status\":\"${GATE_6_STATUS:-PASS}\",\"name\":\"Architecture+BoyScout\",\"bsBlocked\":${BS_BLOCKED}},"
  GATES_JSON="${GATES_JSON}\"gate7\":{\"status\":\"${GATE_7_STATUS:-PASS}\",\"name\":\"IaC Security\"},"
  GATES_JSON="${GATES_JSON}\"gate8\":{\"status\":\"${GATE_8_STATUS:-PASS}\",\"name\":\"Secret Scanning\"},"
  GATES_JSON="${GATES_JSON}\"gate9\":{\"status\":\"${GATE_9_STATUS:-PASS}\",\"name\":\"Build Integrity\"},"
  GATES_JSON="${GATES_JSON}\"gate10\":{\"status\":\"${GATE_10_STATUS:-PASS}\",\"name\":\"SAST Security\"},"
  GATES_JSON="${GATES_JSON}\"gate11\":{\"status\":\"${GATE_11_STATUS:-PASS}\",\"name\":\"Sprint Flow\"},"
  GATES_JSON="${GATES_JSON}\"gate12\":{\"status\":\"${GATE_12_STATUS:-WARN}\",\"name\":\"File Hygiene\"}}"

  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"

  # ── Console output ────────────────────────────────────────────────────────
  echo ""
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  📊 Quality Report — $TIMESTAMP"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo ""
  printf "  %-45s %s\n" "Gate 1: Code Quality" "${GATE_1_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 2: Duplicate Code" "${GATE_2_STATUS:-PASS}"
  printf "  %-45s %s (warnings: ${CC_WARNINGS:-0})\n" "Gate 3: Complexity" "${GATE_3_STATUS:-PASS}"
  printf "  %-45s %s (warnings: ${WARNING_COUNT:-0})\n" "Gate 4: Principles" "${GATE_4_STATUS:-PASS}"
  printf "  %-45s %s (coverage: ${COV_PCT}%%)\n" "Gate 5: Tests + Coverage" "${GATE_5_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 6: Architecture + Boy Scout" "${GATE_6_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 7: IaC Security" "${GATE_7_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 8: Secret Scanning" "${GATE_8_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 9: Build Integrity" "${GATE_9_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 10: SAST Security" "${GATE_10_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 11: Sprint Flow" "${GATE_11_STATUS:-PASS}"
  printf "  %-45s %s\n" "Gate 12: File Hygiene" "${GATE_12_STATUS:-WARN}"
  echo ""
  echo "  Overall Score: $SCORE/10 | $PASSED_COUNT/$TOTAL_GATES gates passed"
  echo "  Branch status: $REPORT_FILE"
  echo "  Run report: $PERSISTED_REPORT_FILE"
  echo "  History saved: $HISTORY_FILE"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo ""
  # AI-friendly common footer: tells LLM agents how to fix issues instead of bypassing
  echo "  ── AI AGENT DIRECTIVE ──"
  echo "  If any gate FAILED above:"
  echo "    - Each error includes a file path and rule identifier"
  echo "    - Fix each issue, then re-run: git commit"
  echo "    - Do NOT use --no-verify — bypassed commits block sprint progression"
  echo "  ─────────────────────────"
  echo ""
}

# Post-commit bypass marker: write a sentinel file in .git/ directory
# that the post-commit hook checks to detect --no-verify bypasses
_wp_marker="$(git rev-parse --git-dir 2>/dev/null)/xp-gate-precommit-marker"
if [ -n "$_wp_marker" ] && [ "$_wp_marker" != "/xp-gate-precommit-marker" ]; then
  echo "$(date +%s)" > "$_wp_marker"
fi

generate_quality_report
_QUALITY_REPORT_DONE=1

exit 0
