Skill: suhteevah/concurrencyguard (version 1.0.1)

--- name: concurrencyguard description: Race condition & concurrency safety analyzer -- detects unprotected shared state, missing locks, TOCTOU vulnerabilities, async/await pitfalls, thread-unsafe singletons, and deadlock-prone patterns across all languages homepage: https://concurrencyguard.pages.dev metadata: { "openclaw": { "emoji": "\u26a1", "primaryEnv": "CONCURRENCYGUARD_LICENSE_KEY", "requires": { "bins": ["git", "bash", "python3", "jq"] }, "configPaths": ["~/.openclaw/openclaw.json"], "install": [ { "id": "lefthook", "kind": "brew", "formula": "lefthook", "bins": ["lefthook"], "label": "Install lefthook (git hooks manager)" } ], "os": ["darwin", "linux", "win32"] } } user-invocable: true disable-model-invocation: false --- # ConcurrencyGuard -- Race Condition & Concurrency Safety Analyzer ConcurrencyGuard scans codebases for concurrency hazards: unprotected shared state, missing mutex/locks, TOCTOU vulnerabilities, async/await pitfalls, thread-unsafe singletons, missing transaction isolation, data races in goroutines, unguarded lazy initialization, non-atomic read-modify-write, and deadlock-prone lock ordering -- across JS/TS, Python, Java, Go, Rust, and C#. It uses regex-based pattern matching against 90+ concurrency safety patterns, lefthook for git hook integration, and produces markdown reports with actionable remediation recommendations. 100% local. Zero telemetry. ## Commands ### Free Tier (No license required) #### `concurrencyguard scan [file|directory]` One-shot concurrency safety scan of files or directories. **How to execute:** ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" scan [target] ``` **What it does:** 1. Accepts a file path or directory (defaults to current directory) 2. Discovers all source files (skips .git, node_modules, binaries, images, .min.js) 3. Runs 90+ concurrency safety patterns against each file 4. Respects .gitignore and allowlist files 5. Calculates a concurrency safety score (0-100) per file and overall 6. Grades: A (90-100), B (80-89), C (70-79), D (60-69), F (<60) 7. Outputs findings with: file, line number, check ID, severity, description, recommendation 8. Exit code 0 if score >= 70, exit code 1 if too many issues found 9. Free tier limited to 5 files per scan **Example usage scenarios:** - "Scan my code for race conditions" -> runs `concurrencyguard scan .` - "Check this file for concurrency issues" -> runs `concurrencyguard scan src/server.go` - "Find thread safety problems in my project" -> runs `concurrencyguard scan src/` - "Are there any deadlock risks in my code?" -> runs `concurrencyguard scan .` - "Check for missing locks" -> runs `concurrencyguard scan .` - "Find TOCTOU vulnerabilities" -> runs `concurrencyguard scan .` #### `concurrencyguard hook install` Install git pre-commit hooks that scan staged files for concurrency issues before every commit. **How to execute:** ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" hook install ``` **What it does:** 1. Copies lefthook config to project root 2. Installs lefthook pre-commit hook 3. On every commit: scans all staged files for concurrency hazards, blocks commit if critical/high findings, shows remediation advice #### `concurrencyguard hook uninstall` Remove ConcurrencyGuard git hooks. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" hook uninstall ``` #### `concurrencyguard report [directory]` Generate a markdown concurrency safety report with findings, severity breakdown, and remediation steps. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" report [directory] ``` **What it does:** 1. Runs full scan of the directory 2. Generates a formatted markdown report from template 3. Includes per-file breakdowns, concurrency safety scores, remediation priority 4. Output suitable for code reviews and architecture audits ### Pro Tier ($19/user/month -- requires CONCURRENCYGUARD_LICENSE_KEY) #### `concurrencyguard watch [directory]` Continuous file-watching mode that re-scans on every file change. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" watch [directory] ``` **What it does:** 1. Validates Pro+ license 2. Watches directory for file changes using filesystem events 3. Re-scans changed files automatically 4. Reports new concurrency issues in real time 5. Ideal for active development sessions #### `concurrencyguard ci [directory]` CI/CD integration mode with strict exit codes and machine-readable output. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" ci [directory] ``` **What it does:** 1. Validates Pro+ license 2. Runs full scan of the directory 3. Outputs machine-readable results with exit codes for CI systems 4. Exit 0 = clean, exit 1 = critical/high issues, exit 2 = medium issues 5. Compatible with GitHub Actions, GitLab CI, Jenkins, CircleCI ### Team Tier ($39/user/month -- requires CONCURRENCYGUARD_LICENSE_KEY with team tier) #### `concurrencyguard team-report [directory]` Generate an aggregate team-level concurrency safety report with trend data. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" team-report [directory] ``` **What it does:** 1. Validates Team+ license 2. Runs full scan with aggregation by module/package 3. Generates team-level metrics (hotspot files, worst categories, trend indicators) 4. Includes per-developer breakdown when git blame data is available 5. Suitable for sprint retrospectives and architecture reviews #### `concurrencyguard baseline [directory]` Establish a baseline of known concurrency issues for allowlisting. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" baseline [directory] ``` **What it does:** 1. Validates Team+ license 2. Scans directory and records all current findings as baseline 3. Saves baseline to .concurrencyguard-baseline.json 4. Future scans only report NEW issues not in the baseline 5. Useful for legacy codebases with known accepted concurrency patterns #### `concurrencyguard status` Show license and configuration information. ```bash bash "<SKILL_DIR>/scripts/concurrencyguard.sh" status ``` ## Detected Concurrency Patterns ConcurrencyGuard detects 90+ concurrency safety patterns across 6 categories: | Category | Examples | Severity | |----------|----------|----------| | **Shared State (SS)** | Global mutable variables, unprotected static fields, module-level mutable state, shared variables without volatile/atomic, global object mutation in worker threads | Critical/High | | **Locking & Mutex (LK)** | Missing synchronized in Java, missing lock in C#, missing mutex.Lock() in Go, Lock without Unlock, nested locks (deadlock risk), missing RWMutex, spin locks in user code | Critical/High | | **TOCTOU & Atomicity (TC)** | Check-then-act without synchronization, file exists then open, read-modify-write without CAS, double-checked locking without volatile, non-atomic counters | Critical/High | | **Async/Await Pitfalls (AW)** | Await in loop (sequential not parallel), missing await, async void, fire-and-forget promises, race conditions after await, missing Promise.all | High/Medium | | **Thread Safety (TS)** | Thread-unsafe singleton, HashMap without sync, non-thread-safe datetime, mutable defaults shared across threads, lazy init without double-check | High/Medium | | **Deadlock & Starvation (DL)** | Inconsistent lock ordering, holding lock across external call, channel without timeout, unbuffered channel in goroutine, missing select/default, lock held across await | Critical/High | ## Configuration Users can configure ConcurrencyGuard in `~/.openclaw/openclaw.json`: ```json { "skills": { "entries": { "concurrencyguard": { "enabled": true, "apiKey": "YOUR_LICENSE_KEY_HERE", "config": { "severityThreshold": "high", "ignorePatterns": ["**/test/**", "**/fixtures/**", "**/*.test.*"], "ignoreChecks": [], "allowlistFile": ".concurrencyguard-allowlist", "reportFormat": "markdown" } } } } } ``` ## Important Notes - **Free tier** works immediately with no configuration - **All scanning happens locally** -- no code is sent to external servers - **License validation is offline** -- no phone-home or network calls - Pattern matching only -- no AST parsing, no external dependencies beyond bash - Supports scanning JS/TS, Python, Java, Go, Rust, and C# in a single pass - Git hooks use **lefthook** which must be installed (see install metadata above) - Exit codes: 0 = clean (score >= 70), 1 = issues detected (for CI/CD integration) ## Error Handling - If lefthook is not installed and user tries `hook install`, prompt to install it - If license key is invalid or expired, show clear message with link to https://concurrencyguard.pages.dev/renew - If a file is binary, skip it automatically with no warning - If no scannable files found in target, report clean scan with info message - If .concurrencyguard-allowlist is missing, skip allowlist filtering gracefully ## When to Use ConcurrencyGuard The user might say things like: - "Scan my code for race conditions" - "Find concurrency bugs in my project" - "Check for thread safety issues" - "Are there any deadlock risks in my code?" - "Scan for missing locks or mutexes" - "Find TOCTOU vulnerabilities" - "Check my async/await code for pitfalls" - "Find unprotected shared state" - "Detect data races in my Go code" - "Check for thread-unsafe singletons" - "Scan for concurrency issues before I push" - "Set up pre-commit hooks for concurrency checking" - "Generate a concurrency safety report" - "Find missing synchronization in my Java code" - "Check for non-atomic operations" - "Detect deadlock-prone lock ordering"

path: config/lefthook.yml
content: # ConcurrencyGuard -- lefthook configuration
# Installed by: concurrencyguard hook install
# Documentation: https://concurrencyguard.pages.dev/docs/hooks

pre-commit:
  parallel: true
  commands:
    concurrencyguard-concurrency-scan:
      run: |
        CONCURRENCYGUARD_SKILL_DIR="${CONCURRENCYGUARD_SKILL_DIR:-$HOME/.openclaw/skills/concurrencyguard}"
        if [[ -f "$CONCURRENCYGUARD_SKILL_DIR/scripts/analyzer.sh" ]]; then
          source "$CONCURRENCYGUARD_SKILL_DIR/scripts/patterns.sh"
          source "$CONCURRENCYGUARD_SKILL_DIR/scripts/analyzer.sh"
          hook_concurrency_scan
        else
          echo "[ConcurrencyGuard] Skill not found at $CONCURRENCYGUARD_SKILL_DIR -- skipping scan"
        fi
      fail_text: |
        Concurrency safety issues detected in staged files!
        Run 'concurrencyguard scan' to see details.
        Or 'git commit --no-verify' to skip (NOT recommended).


path: README.md
content: # concurrencyguard

<p align="center">
  <img src="https://img.shields.io/badge/patterns-90+-blue" alt="90+ patterns">
  <img src="https://img.shields.io/badge/categories-6-purple" alt="6 categories">
  <img src="https://img.shields.io/badge/languages-6-orange" alt="6 languages">
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
  <img src="https://img.shields.io/badge/install-clawhub-blue" alt="ClawHub">
  <img src="https://img.shields.io/badge/zero-telemetry-brightgreen" alt="Zero telemetry">
</p>

<h3 align="center">Find the race conditions hiding in your code. Fix them before production does.</h3>

<p align="center">
  <a href="https://concurrencyguard.pages.dev">Website</a> &middot;
  <a href="#quick-start">Quick Start</a> &middot;
  <a href="#detection-categories">Categories</a> &middot;
  <a href="https://concurrencyguard.pages.dev/#pricing">Pricing</a>
</p>

---

## Your codebase has concurrency bugs. Hidden ones.

Race conditions are the hardest bugs to find. They happen intermittently. They pass all your tests. They only surface under load in production -- corrupting data, causing deadlocks, producing wrong results silently.

Unprotected shared state. Missing mutexes. Check-then-act without synchronization. Await inside loops instead of Promise.all. Thread-unsafe singletons. Inconsistent lock ordering. Fire-and-forget promises. Non-atomic read-modify-write operations.

These patterns are predictable. They follow recognizable code shapes. ConcurrencyGuard finds them.

**90+ concurrency safety patterns across 6 languages.** JS/TS, Python, Java, Go, Rust, C#. Pre-commit hooks. Local scanning. Markdown reports. Zero data leaves your machine.

## Quick Start

```bash
# 1. Install via ClawHub (free)
clawhub install concurrencyguard

# 2. Scan your repo
concurrencyguard scan

# 3. Install pre-commit hooks (free)
concurrencyguard hook install
```

That's it. Every commit is now scanned for concurrency issues before it lands.

## What It Does

### Scan files for concurrency hazards
One command to scan any file, directory, or your entire repo. 90+ regex patterns detect concurrency issues across shared state, locking, atomicity, async/await, thread safety, and deadlock risk.

### Block commits with pre-commit hooks
Install a lefthook pre-commit hook that scans every staged file. If concurrency issues are detected, the commit is blocked with a clear remediation message.

### Generate concurrency safety reports
Produce markdown reports with severity breakdowns, hazard categories, and remediation priority. Ideal for code reviews and architecture audits.

### Continuous watch mode (Pro)
Monitor your codebase in real time during development. Every file save triggers a re-scan and immediate feedback on new concurrency issues.

### CI/CD integration (Pro)
Strict exit codes and machine-readable output for GitHub Actions, GitLab CI, Jenkins, and any CI system.

### Team-level reporting (Team)
Aggregate concurrency metrics across your team with hotspot analysis, per-module breakdowns, and trend indicators for sprint retrospectives.

### Baseline known issues (Team)
Record existing concurrency issues in legacy codebases so future scans only report new hazards. Allowlist files let teams manage known exceptions.

## How It Compares

| Feature | ConcurrencyGuard | ThreadSanitizer | go vet | RacerD | CHESS |
|---------|:----------------:|:---------------:|:------:|:------:|:-----:|
| Static analysis (no runtime) | Yes | No | Partial | Yes | No |
| Multi-language (6 langs) | Yes | C/C++ | Go only | Java | C# |
| Pre-commit hooks | Yes | No | No | No | No |
| Score & grades | Yes | No | No | No | No |
| Baseline/allowlist | Yes | No | No | No | No |
| Zero config scan | Yes | Build integration | Go toolchain | Build integration | Build integration |
| No binary deps | Yes | LLVM | Go toolchain | Build system | .NET |
| Markdown reports | Yes | No | No | No | No |
| CI/CD integration | Yes | Manual | Manual | Manual | Manual |
| Local-only (no cloud) | Yes | Yes | Yes | Yes | Yes |
| Zero telemetry | Yes | Yes | Yes | Yes | Yes |
| ClawHub integration | Yes | No | No | No | No |
| Price (individual) | Free/$19/mo | Free | Free | Free | Free |

## Detection Categories

ConcurrencyGuard detects 90+ concurrency safety patterns across 6 categories:

### Shared State (SS) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| SS-001 | Critical | Global mutable variable accessed without synchronization |
| SS-002 | Critical | Unprotected static mutable field in Java/C# |
| SS-003 | High | Module-level mutable state in Python (shared across threads) |
| SS-004 | High | Shared variable without volatile/atomic annotation |
| SS-005 | Critical | Global object mutation in Node.js worker threads |
| SS-006 | High | Mutable class variable shared between threads |
| SS-007 | High | Static collection modified without synchronization |
| SS-008 | Medium | Unprotected global counter or accumulator |
| SS-009 | High | Shared mutable Map/Dict without concurrent variant |
| SS-010 | Critical | Go global variable modified in goroutine |
| SS-011 | High | Rust unsafe mutable static reference |
| SS-012 | Medium | Module-level list/array mutation in Python |
| SS-013 | High | Shared buffer/array without synchronization |
| SS-014 | Medium | Global configuration object modified at runtime |
| SS-015 | High | Thread-local storage not used for per-thread state |

### Locking & Mutex (LK) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| LK-001 | Critical | Missing synchronized keyword on shared method in Java |
| LK-002 | Critical | Missing lock statement in C# for shared resource |
| LK-003 | Critical | Missing mutex.Lock()/Unlock() in Go for shared state |
| LK-004 | Critical | Lock acquired without corresponding Unlock/Release |
| LK-005 | High | Nested lock acquisition (deadlock risk) |
| LK-006 | High | Missing RWMutex for read-heavy shared data in Go |
| LK-007 | Medium | Spin lock implementation in user code |
| LK-008 | High | Lock not deferred in Go (risk of missing unlock on panic) |
| LK-009 | High | Missing threading.Lock() in Python for shared state |
| LK-010 | Critical | Lock/Unlock on different code paths (conditional unlock) |
| LK-011 | Medium | Using basic Lock where RLock is appropriate |
| LK-012 | High | Mutex not held during condition variable wait |
| LK-013 | High | Missing lock in C# property getter/setter |
| LK-014 | Medium | Manual lock implementation instead of language primitives |
| LK-015 | High | Sync.Mutex value copied in Go (pass by value) |

### TOCTOU & Atomicity (TC) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| TC-001 | Critical | Check-then-act without synchronization (generic) |
| TC-002 | Critical | File exists check followed by file open (TOCTOU) |
| TC-003 | Critical | Read-modify-write without compare-and-swap (CAS) |
| TC-004 | High | Double-checked locking without volatile/atomic |
| TC-005 | Critical | Non-atomic counter increment (i++ on shared variable) |
| TC-006 | High | Compare-and-swap needed for conditional update |
| TC-007 | Critical | Get-then-put on Map without atomic operation |
| TC-008 | High | Test-and-set without atomic instruction |
| TC-009 | Critical | Non-atomic boolean flag for thread communication |
| TC-010 | High | Check-if-null-then-create without synchronization |
| TC-011 | Critical | Balance check then debit without transaction isolation |
| TC-012 | High | Collection size check followed by index access |
| TC-013 | Medium | Non-atomic file write (no rename-based atomic write) |
| TC-014 | High | Database read-check-write without SELECT FOR UPDATE |
| TC-015 | Critical | Non-atomic publish of multi-field state |

### Async/Await Pitfalls (AW) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| AW-001 | High | Await inside loop (sequential instead of parallel) |
| AW-002 | Critical | Missing await on async function call |
| AW-003 | High | Async void method (unobserved exceptions in C#) |
| AW-004 | Critical | Fire-and-forget promise (no .catch or await) |
| AW-005 | High | Race condition in state update after await point |
| AW-006 | High | Missing Promise.all/Promise.allSettled for parallel ops |
| AW-007 | Medium | Async function without try/catch error handling |
| AW-008 | High | Shared mutable state across await boundaries |
| AW-009 | Critical | Multiple awaits on same promise (double resolution) |
| AW-010 | High | Callback-based API mixed with async/await |
| AW-011 | Medium | Missing error propagation in promise chain |
| AW-012 | High | Async generator without proper cleanup |
| AW-013 | Critical | Unhandled promise rejection (no rejection handler) |
| AW-014 | Medium | Sequential async calls that could be parallel |
| AW-015 | High | Holding resource lock across await point |

### Thread Safety (TS) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| TS-001 | Critical | Thread-unsafe singleton initialization |
| TS-002 | Critical | HashMap/ArrayList without synchronization in Java |
| TS-003 | High | Non-thread-safe datetime formatting (SimpleDateFormat) |
| TS-004 | High | Mutable default arguments shared across threads in Python |
| TS-005 | Critical | Lazy initialization without double-check locking |
| TS-006 | High | StringBuilder shared across threads in Java/C# |
| TS-007 | High | Regular expression object shared across threads |
| TS-008 | Critical | Non-thread-safe cache implementation |
| TS-009 | High | Shared Random instance without synchronization |
| TS-010 | Medium | Mutable return value from synchronized method |
| TS-011 | High | Thread-unsafe event handler registration |
| TS-012 | Critical | Publishing this reference in constructor |
| TS-013 | High | Non-synchronized iteration over shared collection |
| TS-014 | Medium | Using thread-unsafe collection in concurrent context |
| TS-015 | High | Missing @ThreadSafe/@GuardedBy annotation |

### Deadlock & Starvation (DL) -- 15 patterns

| Check ID | Severity | Description |
|----------|----------|-------------|
| DL-001 | Critical | Inconsistent lock ordering across methods |
| DL-002 | High | Holding lock while calling external/virtual method |
| DL-003 | Critical | Channel operation without timeout in Go |
| DL-004 | High | Unbuffered channel send in goroutine (potential block) |
| DL-005 | High | Missing select with default for non-blocking channel op |
| DL-006 | Critical | Lock held across await point (async deadlock) |
| DL-007 | High | Recursive lock acquisition (reentrant deadlock) |
| DL-008 | Critical | Acquiring multiple locks without consistent ordering |
| DL-009 | High | Blocking call inside synchronized block |
| DL-010 | Medium | Thread.sleep inside lock (starvation risk) |
| DL-011 | High | Missing timeout on lock acquisition |
| DL-012 | Critical | Goroutine leak (spawned without cancellation context) |
| DL-013 | High | WaitGroup.Add called inside goroutine |
| DL-014 | Medium | Reader-writer lock with writer starvation risk |
| DL-015 | High | Semaphore acquire without timeout |

## Pricing

| Feature | Free | Pro ($19/user/mo) | Team ($39/user/mo) |
|---------|:----:|:------------------:|:-------------------:|
| One-shot scan | 5 files | Unlimited | Unlimited |
| 90+ detection patterns | Yes | Yes | Yes |
| 6 language support | Yes | Yes | Yes |
| Pre-commit hooks | Yes | Yes | Yes |
| Concurrency reports | Yes | Yes | Yes |
| Watch mode | | Yes | Yes |
| CI/CD integration | | Yes | Yes |
| Team-level reports | | | Yes |
| Baseline/allowlist | | | Yes |
| Custom policy rules | | | Yes |

## Configuration

Add to `~/.openclaw/openclaw.json`:

```json
{
  "skills": {
    "entries": {
      "concurrencyguard": {
        "enabled": true,
        "apiKey": "YOUR_LICENSE_KEY",
        "config": {
          "severityThreshold": "high",
          "ignorePatterns": ["**/test/**", "**/fixtures/**", "**/*.test.*"],
          "ignoreChecks": [],
          "allowlistFile": ".concurrencyguard-allowlist",
          "reportFormat": "markdown"
        }
      }
    }
  }
}
```

## Ecosystem

ConcurrencyGuard is part of the ClawHub code quality suite:

- **[SecretScan](https://secretscan.pages.dev)** -- Hardcoded secrets & credential leak detection
- **[AccessLint](https://accesslint.pages.dev)** -- Accessibility compliance scanning
- **[StyleGuard](https://styleguard.pages.dev)** -- Code style & consistency enforcement
- **[EnvGuard](https://envguard.pages.dev)** -- Environment variable safety
- **[DepGuard](https://depguard.pages.dev)** -- Dependency vulnerability scanning
- **[ConfigSafe](https://configsafe.pages.dev)** -- Infrastructure configuration auditing
- **[APIShield](https://apishield.pages.dev)** -- API security scanning
- **[DeadCode](https://deadcode.pages.dev)** -- Dead code and unused export detection
- **[TypeDrift](https://typedrift.pages.dev)** -- Type safety drift detection
- **[PerfGuard](https://perfguard.pages.dev)** -- Performance regression detection
- **[GitPulse](https://gitpulse.pages.dev)** -- Git workflow analytics
- **[DocSync](https://docsync.pages.dev)** -- Documentation drift detection
- **[SQLGuard](https://sqlguard.pages.dev)** -- SQL injection & query safety detection
- **[MemGuard](https://memguard.pages.dev)** -- Memory safety & leak detection
- **[MigrateSafe](https://migratesafe.pages.dev)** -- Database migration safety checking
- **[LicenseGuard](https://licenseguard.pages.dev)** -- License compliance scanning
- **[ConcurrencyGuard](https://concurrencyguard.pages.dev)** -- Race condition & concurrency safety analysis

## Privacy

- 100% local -- no code sent externally
- Zero telemetry
- Offline license validation
- Pattern matching only -- no AST parsing, no external dependencies beyond bash

## License

MIT


path: scripts/analyzer.sh
content: #!/usr/bin/env bash
# ConcurrencyGuard -- Core Analysis Engine
# Provides: file discovery, pattern scanning, .gitignore-aware scanning,
#           allowlist support, score calculation, grades, report generation,
#           hook integration, watch mode, CI mode, team reports, baseline management.
#
# This file is sourced by concurrencyguard.sh and by the lefthook pre-commit hook.
# Requires patterns.sh to be sourced first.

set -euo pipefail

# --- Colors (safe to re-declare; sourcing scripts may set these) --------------

RED="${RED:-\033[0;31m}"
GREEN="${GREEN:-\033[0;32m}"
YELLOW="${YELLOW:-\033[1;33m}"
BLUE="${BLUE:-\033[0;34m}"
CYAN="${CYAN:-\033[0;36m}"
MAGENTA="${MAGENTA:-\033[0;35m}"
BOLD="${BOLD:-\033[1m}"
DIM="${DIM:-\033[2m}"
NC="${NC:-\033[0m}"

SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
SKILL_DIR="${SKILL_DIR:-$(dirname "$SCRIPT_DIR")}"
CONCURRENCYGUARD_VERSION="${VERSION:-1.0.0}"

OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"

# Global findings array
declare -a FINDINGS=()

# ============================================================================
# File Discovery
# ============================================================================

# Source file extensions that are relevant for concurrency analysis
is_concurrency_relevant() {
  local filepath="$1"
  local basename_f
  basename_f=$(basename "$filepath")
  local ext="${basename_f##*.}"
  local ext_lower
  ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')

  case "$ext_lower" in
    # JavaScript / TypeScript
    js|jsx|ts|tsx|mjs|cjs) return 0 ;;
    # Python
    py|pyw) return 0 ;;
    # Java
    java) return 0 ;;
    # Go
    go) return 0 ;;
    # Rust
    rs) return 0 ;;
    # C#
    cs) return 0 ;;
    # Kotlin
    kt|kts) return 0 ;;
    # Scala
    scala) return 0 ;;
    # C/C++
    c|h|cpp|hpp|cc|hh|cxx|hxx) return 0 ;;
    # Ruby
    rb) return 0 ;;
    # PHP
    php) return 0 ;;
    # Swift
    swift) return 0 ;;
    # SQL (for transaction patterns)
    sql) return 0 ;;
    # Config / build files
    yaml|yml|toml|gradle) return 0 ;;
    *) return 1 ;;
  esac
}

# Check if a file should be skipped (binary, generated, vendor, etc.)
should_skip_file() {
  local filepath="$1"
  local basename_f
  basename_f=$(basename "$filepath")
  local basename_lower
  basename_lower=$(echo "$basename_f" | tr '[:upper:]' '[:lower:]')
  local ext="${basename_lower##*.}"

  # Skip binary/image/compiled file extensions
  case "$ext" in
    png|jpg|jpeg|gif|bmp|ico|svg|webp|mp3|mp4|avi|mov|mkv|wav|flac|ogg) return 0 ;;
    zip|tar|gz|bz2|xz|7z|rar|tgz) return 0 ;;
    exe|dll|so|dylib|o|a|class|pyc|pyo|wasm) return 0 ;;
    woff|woff2|ttf|eot|otf) return 0 ;;
    pdf|doc|docx|xls|xlsx|ppt|pptx) return 0 ;;
    lock) return 0 ;;
    map) return 0 ;;
  esac

  # Skip minified files
  case "$basename_lower" in
    *.min.js|*.min.css|*.min.mjs) return 0 ;;
    *.bundle.js|*.chunk.js) return 0 ;;
  esac

  # Skip known non-source files
  case "$basename_lower" in
    package-lock.json|yarn.lock|pnpm-lock.yaml|composer.lock|gemfile.lock|cargo.lock|poetry.lock) return 0 ;;
    .ds_store|thumbs.db|desktop.ini) return 0 ;;
  esac

  # Only analyze concurrency-relevant file types
  if ! is_concurrency_relevant "$filepath"; then
    return 0
  fi

  return 1
}

# Check if a path is in a directory that should be skipped
is_excluded_dir() {
  local filepath="$1"

  case "$filepath" in
    */.git/*|*/.git) return 0 ;;
    */node_modules/*|*/node_modules) return 0 ;;
    */vendor/*|*/vendor) return 0 ;;
    */.next/*|*/.next) return 0 ;;
    */dist/*|*/dist) return 0 ;;
    */build/*|*/build) return 0 ;;
    */coverage/*|*/coverage) return 0 ;;
    */__pycache__/*|*/__pycache__) return 0 ;;
    */.venv/*|*/.venv) return 0 ;;
    */venv/*|*/venv) return 0 ;;
    */.tox/*|*/.tox) return 0 ;;
    */.eggs/*|*/.eggs) return 0 ;;
    */.terraform/*|*/.terraform) return 0 ;;
    */.cache/*|*/.cache) return 0 ;;
    */target/*|*/target) return 0 ;;
    */bin/*|*/obj/*) return 0 ;;
  esac

  return 1
}

# Check if a file is ignored by .gitignore
is_gitignored() {
  local filepath="$1"

  if command -v git &>/dev/null; then
    if git rev-parse --git-dir &>/dev/null 2>&1; then
      if git check-ignore -q "$filepath" 2>/dev/null; then
        return 0
      fi
    fi
  fi

  return 1
}

# Find all scannable files in a directory tree
find_scannable_files() {
  local search_dir="$1"
  local -n _result_files="$2"
  _result_files=()

  if [[ -f "$search_dir" ]]; then
    if ! should_skip_file "$search_dir"; then
      _result_files+=("$search_dir")
    fi
    return 0
  fi

  while IFS= read -r -d '' file; do
    # Skip excluded directories
    is_excluded_dir "$file" && continue

    # Skip binary/generated files
    should_skip_file "$file" && continue

    # Skip gitignored files
    is_gitignored "$file" && continue

    _result_files+=("$file")
  done < <(find "$search_dir" -maxdepth 10 \
    \( -name ".git" -o -name "node_modules" -o -name "vendor" -o -name ".next" \
       -o -name "dist" -o -name "build" -o -name "coverage" -o -name "__pycache__" \
       -o -name ".venv" -o -name "venv" -o -name ".tox" -o -name ".terraform" \
       -o -name ".cache" -o -name "target" -o -name "bin" -o -name "obj" \) -prune -o \
    -type f -print0 2>/dev/null)
}

# ============================================================================
# Allowlist Support
# ============================================================================

declare -a ALLOWLIST_PATTERNS=()

load_allowlist() {
  local search_dir="${1:-.}"
  ALLOWLIST_PATTERNS=()

  local allowlist_file=""

  if [[ -f "$search_dir/.concurrencyguard-allowlist" ]]; then
    allowlist_file="$search_dir/.concurrencyguard-allowlist"
  fi

  if [[ -z "$allowlist_file" ]] && command -v git &>/dev/null; then
    local repo_root
    repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
    if [[ -n "$repo_root" && -f "$repo_root/.concurrencyguard-allowlist" ]]; then
      allowlist_file="$repo_root/.concurrencyguard-allowlist"
    fi
  fi

  # Read from openclaw.json config
  local config_allowlist=""
  if [[ -f "$OPENCLAW_CONFIG" ]]; then
    if command -v python3 &>/dev/null; then
      config_allowlist=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    af = cfg.get('skills', {}).get('entries', {}).get('concurrencyguard', {}).get('config', {}).get('allowlistFile', '')
    print(af)
except: pass
" 2>/dev/null)
    fi

    if [[ -n "$config_allowlist" && -f "$search_dir/$config_allowlist" ]]; then
      allowlist_file="$search_dir/$config_allowlist"
    fi
  fi

  if [[ -n "$allowlist_file" && -f "$allowlist_file" ]]; then
    while IFS= read -r line; do
      [[ -z "$line" || "$line" == \#* ]] && continue
      ALLOWLIST_PATTERNS+=("$line")
    done < "$allowlist_file"
  fi
}

is_allowlisted() {
  local finding="$1"

  for pattern in "${ALLOWLIST_PATTERNS[@]}"; do
    if echo "$finding" | grep -qF "$pattern" 2>/dev/null; then
      return 0
    fi
  done

  return 1
}

# ============================================================================
# Baseline Support
# ============================================================================

declare -a BASELINE_HASHES=()

load_baseline() {
  local search_dir="${1:-.}"
  BASELINE_HASHES=()

  local baseline_file=""

  if [[ -f "$search_dir/.concurrencyguard-baseline.json" ]]; then
    baseline_file="$search_dir/.concurrencyguard-baseline.json"
  fi

  if [[ -z "$baseline_file" ]] && command -v git &>/dev/null; then
    local repo_root
    repo_root=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
    if [[ -n "$repo_root" && -f "$repo_root/.concurrencyguard-baseline.json" ]]; then
      baseline_file="$repo_root/.concurrencyguard-baseline.json"
    fi
  fi

  if [[ -n "$baseline_file" && -f "$baseline_file" ]]; then
    if command -v python3 &>/dev/null; then
      while IFS= read -r hash; do
        [[ -n "$hash" ]] && BASELINE_HASHES+=("$hash")
      done < <(python3 -c "
import json
try:
    with open('$baseline_file') as f:
        data = json.load(f)
    for h in data.get('hashes', []):
        print(h)
except: pass
" 2>/dev/null)
    fi
  fi
}

finding_hash() {
  local finding="$1"
  local file line severity check_id
  IFS='|' read -r file line severity check_id _ _ _ <<< "$finding"
  echo "${file}:${line}:${check_id}" | md5sum 2>/dev/null | cut -d' ' -f1 || \
  echo "${file}:${line}:${check_id}" | openssl md5 2>/dev/null | awk '{print $NF}' || \
  echo "${file}:${line}:${check_id}"
}

is_baselined() {
  local finding="$1"
  local hash
  hash=$(finding_hash "$finding")

  for baseline_hash in "${BASELINE_HASHES[@]}"; do
    if [[ "$hash" == "$baseline_hash" ]]; then
      return 0
    fi
  done

  return 1
}

# ============================================================================
# Pattern Scanning
# ============================================================================

# Scan a single file against all concurrency pattern categories.
# Appends findings to global FINDINGS array.
# Each finding: FILE|LINE|SEVERITY|CHECK_ID|DESCRIPTION|RECOMMENDATION|MATCHED_TEXT
scan_file_with_patterns() {
  local filepath="$1"

  local categories
  categories=$(get_all_concurrencyguard_categories)

  for category in $categories; do
    local patterns_name
    patterns_name=$(get_concurrencyguard_patterns_for_category "$category")

    if [[ -z "$patterns_name" ]]; then
      continue
    fi

    local -n _patterns_ref="$patterns_name"

    for entry in "${_patterns_ref[@]}"; do
      IFS='|' read -r regex severity check_id description recommendation <<< "$entry"

      # Use grep -nE to find matches with line numbers
      local matches
      matches=$(grep -nE "$regex" "$filepath" 2>/dev/null || true)

      if [[ -n "$matches" ]]; then
        while IFS= read -r match_line; do
          [[ -z "$match_line" ]] && continue
          local line_num="${match_line%%:*}"
          local matched_text="${match_line#*:}"
          # Trim whitespace
          matched_text=$(echo "$matched_text" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
          # Truncate long lines
          if [[ ${#matched_text} -gt 120 ]]; then
            matched_text="${matched_text:0:117}..."
          fi

          local finding="${filepath}|${line_num}|${severity}|${check_id}|${description}|${recommendation}|${matched_text}"

          # Check allowlist
          if [[ ${#ALLOWLIST_PATTERNS[@]} -gt 0 ]] && is_allowlisted "$finding"; then
            continue
          fi

          # Check baseline
          if [[ ${#BASELINE_HASHES[@]} -gt 0 ]] && is_baselined "$finding"; then
            continue
          fi

          FINDINGS+=("$finding")
        done <<< "$matches"
      fi
    done
  done
}

# ============================================================================
# Score Calculation & Grading
# ============================================================================

# Calculate a concurrency safety score based on findings.
# Score starts at 100 (clean) and decreases with each finding.
# critical=25pts, high=15pts, medium=8pts, low=3pts
calculate_score() {
  local score=100

  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _file _line severity _check_id _desc _rec _text <<< "$finding"
    local points
    points=$(severity_to_points "$severity")
    score=$((score - points))
  done

  # Floor at 0
  if [[ $score -lt 0 ]]; then
    score=0
  fi

  echo "$score"
}

# Convert score to letter grade
score_to_grade() {
  local score="$1"
  if [[ $score -ge 90 ]]; then echo "A"
  elif [[ $score -ge 80 ]]; then echo "B"
  elif [[ $score -ge 70 ]]; then echo "C"
  elif [[ $score -ge 60 ]]; then echo "D"
  else echo "F"
  fi
}

# Grade color
grade_color() {
  local grade="$1"
  case "$grade" in
    A) echo "$GREEN" ;;
    B) echo "$GREEN" ;;
    C) echo "$YELLOW" ;;
    D) echo "$YELLOW" ;;
    F) echo "$RED" ;;
    *) echo "$NC" ;;
  esac
}

# Severity color
severity_color() {
  local severity="$1"
  case "$severity" in
    critical) echo "$RED" ;;
    high)     echo "$MAGENTA" ;;
    medium)   echo "$YELLOW" ;;
    low)      echo "$CYAN" ;;
    *)        echo "$NC" ;;
  esac
}

# ============================================================================
# Output Formatting
# ============================================================================

print_findings() {
  local count=0
  local crit_count=0
  local high_count=0
  local med_count=0
  local low_count=0

  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r file line severity check_id description recommendation matched_text <<< "$finding"
    count=$((count + 1))

    case "$severity" in
      critical) crit_count=$((crit_count + 1)) ;;
      high)     high_count=$((high_count + 1)) ;;
      medium)   med_count=$((med_count + 1)) ;;
      low)      low_count=$((low_count + 1)) ;;
    esac

    local sev_col
    sev_col=$(severity_color "$severity")

    echo -e "  ${sev_col}${severity^^}${NC} ${DIM}${check_id}${NC} ${file}:${line}"
    echo -e "    ${description}"
    echo -e "    ${DIM}Matched: ${matched_text}${NC}"
    echo -e "    ${DIM}Fix: ${recommendation}${NC}"
    echo ""
  done

  echo -e "${BOLD}--- Summary ---${NC}"
  echo -e "  Total findings: ${BOLD}$count${NC}"
  if [[ $crit_count -gt 0 ]]; then
    echo -e "  ${RED}Critical: $crit_count${NC}"
  fi
  if [[ $high_count -gt 0 ]]; then
    echo -e "  ${MAGENTA}High: $high_count${NC}"
  fi
  if [[ $med_count -gt 0 ]]; then
    echo -e "  ${YELLOW}Medium: $med_count${NC}"
  fi
  if [[ $low_count -gt 0 ]]; then
    echo -e "  ${CYAN}Low: $low_count${NC}"
  fi
}

print_score() {
  local score="$1"
  local grade
  grade=$(score_to_grade "$score")
  local gc
  gc=$(grade_color "$grade")

  echo ""
  echo -e "${BOLD}Concurrency Safety Score: ${gc}${score}/100${NC} (Grade: ${gc}${BOLD}${grade}${NC})"

  if [[ $score -ge 70 ]]; then
    echo -e "${GREEN}PASS${NC} -- Score is above threshold (70)"
  else
    echo -e "${RED}FAIL${NC} -- Score is below threshold (70)"
  fi
}

# ============================================================================
# Main Scan Function (FREE)
# ============================================================================

# do_concurrency_scan <target> <file_limit>
# file_limit: max files for free tier (0 = unlimited)
do_concurrency_scan() {
  local target="${1:-.}"
  local file_limit="${2:-0}"
  FINDINGS=()

  # Load allowlist
  load_allowlist "$target"

  # Load baseline if exists
  load_baseline "$target"

  # Discover files
  local -a files=()
  find_scannable_files "$target" files

  local total_files=${#files[@]}

  if [[ $total_files -eq 0 ]]; then
    echo -e "${GREEN}[ConcurrencyGuard]${NC} No scannable source files found in ${BOLD}$target${NC}"
    echo -e "  Clean scan -- no files to check."
    return 0
  fi

  # Apply file limit for free tier
  local scan_count=$total_files
  local limited=false
  if [[ $file_limit -gt 0 && $total_files -gt $file_limit ]]; then
    scan_count=$file_limit
    limited=true
    echo -e "${YELLOW}[ConcurrencyGuard]${NC} Free tier: scanning $file_limit of $total_files files"
    echo -e "  ${DIM}Upgrade to Pro for unlimited scans: https://concurrencyguard.pages.dev/pricing${NC}"
    echo ""
  fi

  echo -e "${BLUE}[ConcurrencyGuard]${NC} Scanning ${BOLD}$scan_count${NC} files for concurrency issues..."
  echo ""

  local i=0
  for filepath in "${files[@]}"; do
    if [[ $file_limit -gt 0 && $i -ge $file_limit ]]; then
      break
    fi

    scan_file_with_patterns "$filepath"
    i=$((i + 1))
  done

  # Print results
  if [[ ${#FINDINGS[@]} -eq 0 ]]; then
    echo -e "${GREEN}${BOLD}No concurrency issues detected!${NC}"
    echo ""
    echo -e "Concurrency Safety Score: ${GREEN}${BOLD}100/100${NC} (Grade: ${GREEN}${BOLD}A${NC})"
    echo -e "${GREEN}PASS${NC}"
    return 0
  fi

  print_findings

  local score
  score=$(calculate_score)
  print_score "$score"

  if [[ $limited == true ]]; then
    echo ""
    echo -e "${YELLOW}Note:${NC} Only $file_limit of $total_files files were scanned (free tier limit)."
    echo -e "  Upgrade to Pro for a complete scan: ${CYAN}https://concurrencyguard.pages.dev/pricing${NC}"
  fi

  # Exit code
  if [[ $score -lt 70 ]]; then
    return 1
  fi
  return 0
}

# ============================================================================
# Report Generation (FREE)
# ============================================================================

generate_concurrency_report() {
  local target="${1:-.}"
  FINDINGS=()

  load_allowlist "$target"
  load_baseline "$target"

  local -a files=()
  find_scannable_files "$target" files

  local total_files=${#files[@]}

  if [[ $total_files -eq 0 ]]; then
    echo -e "${GREEN}[ConcurrencyGuard]${NC} No scannable files found."
    return 0
  fi

  echo -e "${BLUE}[ConcurrencyGuard]${NC} Scanning ${BOLD}$total_files${NC} files for report..." >&2

  for filepath in "${files[@]}"; do
    scan_file_with_patterns "$filepath"
  done

  local score
  score=$(calculate_score)
  local grade
  grade=$(score_to_grade "$score")
  local date_str
  date_str=$(date +%Y-%m-%d 2>/dev/null || echo "unknown")

  # Count severities
  local crit_count=0 high_count=0 med_count=0 low_count=0
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _ _ severity _ _ _ _ <<< "$finding"
    case "$severity" in
      critical) crit_count=$((crit_count + 1)) ;;
      high)     high_count=$((high_count + 1)) ;;
      medium)   med_count=$((med_count + 1)) ;;
      low)      low_count=$((low_count + 1)) ;;
    esac
  done

  local total_issues=${#FINDINGS[@]}

  # Build category breakdown
  local cat_breakdown=""
  local -A cat_counts=()
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _ _ _ check_id _ _ _ <<< "$finding"
    local prefix="${check_id%%-*}"
    cat_counts["$prefix"]=$(( ${cat_counts["$prefix"]:-0} + 1 ))
  done

  for cat in "${!cat_counts[@]}"; do
    local cat_label
    case "$cat" in
      SS) cat_label="Shared State" ;;
      LK) cat_label="Locking & Mutex" ;;
      TC) cat_label="TOCTOU & Atomicity" ;;
      AW) cat_label="Async/Await Pitfalls" ;;
      TS) cat_label="Thread Safety" ;;
      DL) cat_label="Deadlock & Starvation" ;;
      *)  cat_label="$cat" ;;
    esac
    cat_breakdown="${cat_breakdown}| ${cat_label} | ${cat_counts[$cat]} |"$'\n'
  done

  # Build findings table
  local findings_table=""
  local idx=0
  for finding in "${FINDINGS[@]}"; do
    idx=$((idx + 1))
    IFS='|' read -r file line severity check_id description recommendation matched_text <<< "$finding"
    findings_table="${findings_table}| ${idx} | ${file} | ${line} | ${severity} | ${check_id} | ${description} |"$'\n'
  done

  # Build recommendations
  local recommendations=""
  if [[ $crit_count -gt 0 ]]; then
    recommendations="${recommendations}1. **CRITICAL:** ${crit_count} critical concurrency issues found -- fix immediately to prevent data corruption"$'\n'
  fi
  if [[ $high_count -gt 0 ]]; then
    recommendations="${recommendations}2. **HIGH:** ${high_count} high-severity findings -- address in current sprint"$'\n'
  fi
  if [[ $med_count -gt 0 ]]; then
    recommendations="${recommendations}3. **MEDIUM:** ${med_count} medium findings -- review and refactor"$'\n'
  fi
  if [[ $low_count -gt 0 ]]; then
    recommendations="${recommendations}4. **LOW:** ${low_count} low findings -- review for false positives"$'\n'
  fi

  # Get project name
  local project_name
  project_name=$(basename "$(cd "$target" 2>/dev/null && pwd)" 2>/dev/null || echo "$target")

  # Read template and substitute
  local template_file="$SKILL_DIR/templates/report.md.tmpl"
  if [[ ! -f "$template_file" ]]; then
    echo -e "${RED}[ConcurrencyGuard]${NC} Report template not found: $template_file" >&2
    return 1
  fi

  local report
  report=$(cat "$template_file")
  report="${report//\{\{PROJECT\}\}/$project_name}"
  report="${report//\{\{VERSION\}\}/$CONCURRENCYGUARD_VERSION}"
  report="${report//\{\{DATE\}\}/$date_str}"
  report="${report//\{\{CONCURRENCY_SCORE\}\}/$score}"
  report="${report//\{\{GRADE\}\}/$grade}"
  report="${report//\{\{FILES_SCANNED\}\}/$total_files}"
  report="${report//\{\{TOTAL_ISSUES\}\}/$total_issues}"
  report="${report//\{\{CRITICAL_COUNT\}\}/$crit_count}"
  report="${report//\{\{HIGH_COUNT\}\}/$high_count}"
  report="${report//\{\{MEDIUM_COUNT\}\}/$med_count}"
  report="${report//\{\{LOW_COUNT\}\}/$low_count}"
  report="${report//\{\{CATEGORY_BREAKDOWN\}\}/$cat_breakdown}"
  report="${report//\{\{FINDINGS_TABLE\}\}/$findings_table}"
  report="${report//\{\{RECOMMENDATIONS\}\}/$recommendations}"

  echo "$report"

  echo "" >&2
  echo -e "${GREEN}[ConcurrencyGuard]${NC} Report generated. Score: ${BOLD}$score/100${NC} (Grade: ${BOLD}$grade${NC})" >&2
}

# ============================================================================
# Watch Mode (PRO+)
# ============================================================================

watch_mode() {
  local target="${1:-.}"

  echo -e "${BLUE}[ConcurrencyGuard]${NC} Watch mode started on ${BOLD}$target${NC}"
  echo -e "  ${DIM}Press Ctrl+C to stop${NC}"
  echo ""

  # Use filesystem polling as a fallback (works everywhere)
  local -A file_mtimes=()

  # Initial scan
  local -a files=()
  find_scannable_files "$target" files

  for filepath in "${files[@]}"; do
    local mtime
    mtime=$(stat -c %Y "$filepath" 2>/dev/null || stat -f %m "$filepath" 2>/dev/null || echo "0")
    file_mtimes["$filepath"]="$mtime"
  done

  echo -e "${GREEN}[ConcurrencyGuard]${NC} Watching ${#files[@]} files. Initial scan..."
  echo ""

  # Run initial scan
  FINDINGS=()
  load_allowlist "$target"
  load_baseline "$target"

  for filepath in "${files[@]}"; do
    scan_file_with_patterns "$filepath"
  done

  if [[ ${#FINDINGS[@]} -gt 0 ]]; then
    print_findings
    local score
    score=$(calculate_score)
    print_score "$score"
  else
    echo -e "${GREEN}${BOLD}Clean!${NC} No concurrency issues detected."
  fi

  echo ""
  echo -e "${DIM}Watching for changes...${NC}"

  # Poll loop
  while true; do
    sleep 2

    local changed=false
    local -a changed_files=()

    find_scannable_files "$target" files

    for filepath in "${files[@]}"; do
      local mtime
      mtime=$(stat -c %Y "$filepath" 2>/dev/null || stat -f %m "$filepath" 2>/dev/null || echo "0")
      local prev_mtime="${file_mtimes[$filepath]:-0}"

      if [[ "$mtime" != "$prev_mtime" ]]; then
        changed=true
        changed_files+=("$filepath")
        file_mtimes["$filepath"]="$mtime"
      fi
    done

    if [[ "$changed" == true ]]; then
      echo ""
      echo -e "${BLUE}[ConcurrencyGuard]${NC} Changes detected in ${#changed_files[@]} file(s). Rescanning..."

      FINDINGS=()
      for filepath in "${changed_files[@]}"; do
        scan_file_with_patterns "$filepath"
      done

      if [[ ${#FINDINGS[@]} -gt 0 ]]; then
        print_findings
        local score
        score=$(calculate_score)
        print_score "$score"
      else
        echo -e "${GREEN}${BOLD}Clean!${NC} No new concurrency issues."
      fi

      echo ""
      echo -e "${DIM}Watching for changes...${NC}"
    fi
  done
}

# ============================================================================
# CI Mode (PRO+)
# ============================================================================

ci_mode() {
  local target="${1:-.}"
  FINDINGS=()

  load_allowlist "$target"
  load_baseline "$target"

  local -a files=()
  find_scannable_files "$target" files

  local total_files=${#files[@]}

  if [[ $total_files -eq 0 ]]; then
    echo "::notice::ConcurrencyGuard: No scannable files found"
    exit 0
  fi

  for filepath in "${files[@]}"; do
    scan_file_with_patterns "$filepath"
  done

  local score
  score=$(calculate_score)
  local grade
  grade=$(score_to_grade "$score")

  # Output GitHub Actions annotations
  local crit_count=0 high_count=0 med_count=0 low_count=0
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r file line severity check_id description recommendation matched_text <<< "$finding"

    case "$severity" in
      critical)
        crit_count=$((crit_count + 1))
        echo "::error file=${file},line=${line}::${check_id}: ${description}"
        ;;
      high)
        high_count=$((high_count + 1))
        echo "::error file=${file},line=${line}::${check_id}: ${description}"
        ;;
      medium)
        med_count=$((med_count + 1))
        echo "::warning file=${file},line=${line}::${check_id}: ${description}"
        ;;
      low)
        low_count=$((low_count + 1))
        echo "::notice file=${file},line=${line}::${check_id}: ${description}"
        ;;
    esac
  done

  echo ""
  echo "ConcurrencyGuard CI Report"
  echo "========================="
  echo "Score: ${score}/100 (Grade: ${grade})"
  echo "Files scanned: ${total_files}"
  echo "Total issues: ${#FINDINGS[@]}"
  echo "Critical: ${crit_count} | High: ${high_count} | Medium: ${med_count} | Low: ${low_count}"

  # Exit codes: 0=clean, 1=critical/high, 2=medium
  if [[ $crit_count -gt 0 || $high_count -gt 0 ]]; then
    exit 1
  elif [[ $med_count -gt 0 ]]; then
    exit 2
  fi
  exit 0
}

# ============================================================================
# Team Report (TEAM+)
# ============================================================================

team_report() {
  local target="${1:-.}"
  FINDINGS=()

  load_allowlist "$target"
  load_baseline "$target"

  local -a files=()
  find_scannable_files "$target" files

  local total_files=${#files[@]}

  if [[ $total_files -eq 0 ]]; then
    echo -e "${GREEN}[ConcurrencyGuard]${NC} No scannable files found."
    return 0
  fi

  echo -e "${BLUE}[ConcurrencyGuard]${NC} Generating team report for ${BOLD}$total_files${NC} files..." >&2

  for filepath in "${files[@]}"; do
    scan_file_with_patterns "$filepath"
  done

  local score
  score=$(calculate_score)
  local grade
  grade=$(score_to_grade "$score")
  local date_str
  date_str=$(date +%Y-%m-%d 2>/dev/null || echo "unknown")

  # Count severities
  local crit_count=0 high_count=0 med_count=0 low_count=0
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _ _ severity _ _ _ _ <<< "$finding"
    case "$severity" in
      critical) crit_count=$((crit_count + 1)) ;;
      high)     high_count=$((high_count + 1)) ;;
      medium)   med_count=$((med_count + 1)) ;;
      low)      low_count=$((low_count + 1)) ;;
    esac
  done

  # Hotspot analysis: find files with most issues
  local -A file_issue_counts=()
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r file _ _ _ _ _ _ <<< "$finding"
    file_issue_counts["$file"]=$(( ${file_issue_counts["$file"]:-0} + 1 ))
  done

  # Category distribution
  local -A cat_counts=()
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _ _ _ check_id _ _ _ <<< "$finding"
    local prefix="${check_id%%-*}"
    cat_counts["$prefix"]=$(( ${cat_counts["$prefix"]:-0} + 1 ))
  done

  # Output team report
  echo "# ConcurrencyGuard Team Report"
  echo ""
  echo "> Generated on ${date_str} by ConcurrencyGuard v${CONCURRENCYGUARD_VERSION}"
  echo ""
  echo "## Overview"
  echo ""
  echo "| Metric | Value |"
  echo "|--------|-------|"
  echo "| Concurrency Safety Score | **${score}/100** (Grade: **${grade}**) |"
  echo "| Files Scanned | ${total_files} |"
  echo "| Total Issues | ${#FINDINGS[@]} |"
  echo "| Critical | ${crit_count} |"
  echo "| High | ${high_count} |"
  echo "| Medium | ${med_count} |"
  echo "| Low | ${low_count} |"
  echo ""
  echo "## Category Distribution"
  echo ""
  echo "| Category | Count |"
  echo "|----------|-------|"
  for cat in "${!cat_counts[@]}"; do
    local cat_label
    case "$cat" in
      SS) cat_label="Shared State" ;;
      LK) cat_label="Locking & Mutex" ;;
      TC) cat_label="TOCTOU & Atomicity" ;;
      AW) cat_label="Async/Await Pitfalls" ;;
      TS) cat_label="Thread Safety" ;;
      DL) cat_label="Deadlock & Starvation" ;;
      *)  cat_label="$cat" ;;
    esac
    echo "| ${cat_label} | ${cat_counts[$cat]} |"
  done
  echo ""
  echo "## Hotspot Files"
  echo ""
  echo "| File | Issues |"
  echo "|------|--------|"
  # Sort by count (simple approach: iterate)
  for file in "${!file_issue_counts[@]}"; do
    echo "| ${file} | ${file_issue_counts[$file]} |"
  done | sort -t'|' -k3 -rn | head -10
  echo ""

  # Git blame integration if available
  if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
    echo "## Author Distribution"
    echo ""
    echo "| Author | Issues |"
    echo "|--------|--------|"
    local -A author_counts=()
    for finding in "${FINDINGS[@]}"; do
      IFS='|' read -r file line _ _ _ _ _ <<< "$finding"
      if [[ -f "$file" ]]; then
        local author
        author=$(git blame -L "${line},${line}" --porcelain "$file" 2>/dev/null | grep "^author " | sed 's/^author //' || echo "unknown")
        author_counts["$author"]=$(( ${author_counts["$author"]:-0} + 1 ))
      fi
    done
    for author in "${!author_counts[@]}"; do
      echo "| ${author} | ${author_counts[$author]} |"
    done | sort -t'|' -k3 -rn
    echo ""
  fi

  echo "---"
  echo ""
  echo "*Report generated by [ConcurrencyGuard](https://concurrencyguard.pages.dev). Run \`concurrencyguard scan\` for interactive results.*"

  echo "" >&2
  echo -e "${GREEN}[ConcurrencyGuard]${NC} Team report generated. Score: ${BOLD}$score/100${NC} (Grade: ${BOLD}$grade${NC})" >&2
}

# ============================================================================
# Baseline Management (TEAM+)
# ============================================================================

baseline_mode() {
  local target="${1:-.}"
  FINDINGS=()

  # Scan without loading existing baseline
  BASELINE_HASHES=()
  load_allowlist "$target"

  local -a files=()
  find_scannable_files "$target" files

  for filepath in "${files[@]}"; do
    scan_file_with_patterns "$filepath"
  done

  # Compute hashes for all findings
  local hashes_json="["
  local first=true
  for finding in "${FINDINGS[@]}"; do
    local hash
    hash=$(finding_hash "$finding")
    if [[ "$first" == true ]]; then
      first=false
    else
      hashes_json="${hashes_json},"
    fi
    hashes_json="${hashes_json}\"${hash}\""
  done
  hashes_json="${hashes_json}]"

  local date_str
  date_str=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%d 2>/dev/null || echo "unknown")

  local baseline_file="$target/.concurrencyguard-baseline.json"

  if command -v python3 &>/dev/null; then
    python3 -c "
import json
data = {
    'version': '1.0.0',
    'created': '$date_str',
    'tool': 'concurrencyguard',
    'total_findings': ${#FINDINGS[@]},
    'hashes': json.loads('$hashes_json')
}
with open('$baseline_file', 'w') as f:
    json.dump(data, f, indent=2)
print('OK')
" 2>/dev/null
  else
    # Fallback: write JSON manually
    cat > "$baseline_file" <<BASELINE_EOF
{
  "version": "1.0.0",
  "created": "${date_str}",
  "tool": "concurrencyguard",
  "total_findings": ${#FINDINGS[@]},
  "hashes": ${hashes_json}
}
BASELINE_EOF
  fi

  echo -e "${GREEN}[ConcurrencyGuard]${NC} Baseline created: ${BOLD}$baseline_file${NC}"
  echo -e "  ${#FINDINGS[@]} findings baselined."
  echo -e "  Future scans will only report NEW concurrency issues not in this baseline."
}

# ============================================================================
# Hook Integration
# ============================================================================

# Called by lefthook pre-commit hook
hook_concurrency_scan() {
  # Get list of staged files
  local staged_files
  staged_files=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)

  if [[ -z "$staged_files" ]]; then
    return 0
  fi

  FINDINGS=()
  load_allowlist "."
  load_baseline "."

  local file_count=0
  while IFS= read -r filepath; do
    [[ -z "$filepath" ]] && continue
    [[ ! -f "$filepath" ]] && continue

    # Skip excluded dirs and binary files
    is_excluded_dir "$filepath" && continue
    should_skip_file "$filepath" && continue

    scan_file_with_patterns "$filepath"
    file_count=$((file_count + 1))
  done <<< "$staged_files"

  if [[ ${#FINDINGS[@]} -eq 0 ]]; then
    echo -e "${GREEN}[ConcurrencyGuard]${NC} No concurrency issues detected in $file_count staged files."
    return 0
  fi

  # Check if any critical or high findings exist
  local has_blocking=false
  for finding in "${FINDINGS[@]}"; do
    IFS='|' read -r _ _ severity _ _ _ _ <<< "$finding"
    if [[ "$severity" == "critical" || "$severity" == "high" ]]; then
      has_blocking=true
      break
    fi
  done

  echo -e "${RED}${BOLD}[ConcurrencyGuard] Concurrency issues detected in staged files!${NC}"
  echo ""
  print_findings

  local score
  score=$(calculate_score)
  print_score "$score"

  if [[ "$has_blocking" == true ]]; then
    echo ""
    echo -e "${RED}Commit blocked.${NC} Fix concurrency issues before committing."
    echo -e "  Run: ${CYAN}concurrencyguard scan${NC} for details"
    echo -e "  Skip: ${DIM}git commit --no-verify${NC} (not recommended)"
    return 1
  fi

  return 0
}


path: scripts/concurrencyguard.sh
content: #!/usr/bin/env bash
# ConcurrencyGuard -- Main CLI entry point
# Usage: concurrencyguard.sh <command> [args...]
#
# Commands:
#   scan [file|dir]           One-shot concurrency safety scan (FREE, 5 file limit)
#   hook install              Install pre-commit hooks (FREE)
#   hook uninstall            Remove ConcurrencyGuard hooks (FREE)
#   report [dir]              Generate concurrency safety report (FREE)
#   watch [dir]               Continuous file-watching mode (PRO+)
#   ci [dir]                  CI/CD integration with exit codes (PRO+)
#   team-report [dir]         Aggregate team-level report (TEAM+)
#   baseline [dir]            Baseline known issues for allowlisting (TEAM+)
#   status                    Show license and config info

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
VERSION="1.0.0"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

log_info()    { echo -e "${BLUE}[ConcurrencyGuard]${NC} $*"; }
log_success() { echo -e "${GREEN}[ConcurrencyGuard]${NC} $*"; }
log_warn()    { echo -e "${YELLOW}[ConcurrencyGuard]${NC} $*"; }
log_error()   { echo -e "${RED}[ConcurrencyGuard]${NC} $*" >&2; }

show_version() {
  echo -e "${BOLD}ConcurrencyGuard${NC} v${VERSION}"
  echo "  Race condition & concurrency safety analyzer"
  echo "  https://concurrencyguard.pages.dev"
}

show_help() {
  show_version
  echo ""
  echo -e "${BOLD}USAGE${NC}"
  echo "  concurrencyguard <command> [options]"
  echo ""
  echo -e "${BOLD}FREE COMMANDS${NC}"
  echo -e "  scan [file|dir]           ${DIM}[FREE]${NC} One-shot concurrency scan (5 file limit)"
  echo -e "  hook install              ${DIM}[FREE]${NC} Install pre-commit hooks"
  echo -e "  hook uninstall            ${DIM}[FREE]${NC} Remove ConcurrencyGuard hooks"
  echo -e "  report [dir]              ${DIM}[FREE]${NC} Generate concurrency safety report"
  echo ""
  echo -e "${BOLD}PRO COMMANDS${NC} (\$19/user/month)"
  echo -e "  watch [dir]               ${DIM}[PRO]${NC}  Continuous file-watching mode"
  echo -e "  ci [dir]                  ${DIM}[PRO]${NC}  CI/CD integration with exit codes"
  echo ""
  echo -e "${BOLD}TEAM COMMANDS${NC} (\$39/user/month)"
  echo -e "  team-report [dir]         ${DIM}[TEAM]${NC} Aggregate team-level report"
  echo -e "  baseline [dir]            ${DIM}[TEAM]${NC} Baseline known issues for allowlisting"
  echo ""
  echo -e "${BOLD}OTHER${NC}"
  echo "  status                    Show license and config info"
  echo "  patterns                  List all detection patterns"
  echo "  --help, -h                Show this help"
  echo "  --version, -v             Show version"
  echo ""
  echo -e "Get a license at ${CYAN}https://concurrencyguard.pages.dev${NC}"
}

# --- License ----------------------------------------------------------------

require_license() {
  local required_tier="${1:-pro}"
  source "$SCRIPT_DIR/license.sh"
  check_concurrencyguard_license "$required_tier"
}

# --- Hooks management -------------------------------------------------------

do_hooks_install() {
  if ! command -v lefthook &>/dev/null; then
    log_error "lefthook not installed."
    echo "  Install: brew install lefthook"
    echo "  Or: npm install -g lefthook"
    return 1
  fi

  if ! git rev-parse --git-dir &>/dev/null 2>&1; then
    log_error "Not inside a git repository."
    return 1
  fi

  local repo_root
  repo_root=$(git rev-parse --show-toplevel)

  log_info "Installing hooks in ${BOLD}$repo_root${NC}"

  local config="$repo_root/lefthook.yml"

  if [[ -f "$config" ]]; then
    if grep -q "concurrencyguard" "$config" 2>/dev/null; then
      log_success "Hooks already configured."
      return 0
    fi

    # Append to existing config
    cat >> "$config" <<'HOOKS'

# --- ConcurrencyGuard hooks ----------------------------------------
    concurrencyguard-concurrency-scan:
      run: |
        CONCURRENCYGUARD_SKILL_DIR="${CONCURRENCYGUARD_SKILL_DIR:-$HOME/.openclaw/skills/concurrencyguard}"
        if [[ -f "$CONCURRENCYGUARD_SKILL_DIR/scripts/analyzer.sh" ]]; then
          source "$CONCURRENCYGUARD_SKILL_DIR/scripts/patterns.sh"
          source "$CONCURRENCYGUARD_SKILL_DIR/scripts/analyzer.sh"
          hook_concurrency_scan
        else
          echo "[ConcurrencyGuard] Skill not found at $CONCURRENCYGUARD_SKILL_DIR -- skipping scan"
        fi
      fail_text: |
        Concurrency safety issues detected in staged files!
        Run 'concurrencyguard scan' to see details.
        Or 'git commit --no-verify' to skip (NOT recommended).
HOOKS
    echo -e "${GREEN}+${NC} Appended ConcurrencyGuard hooks to existing lefthook.yml"
  else
    cp "$SKILL_DIR/config/lefthook.yml" "$config"
    echo -e "${GREEN}+${NC} Created lefthook.yml"
  fi

  (cd "$repo_root" && lefthook install)
  echo ""
  log_success "Hooks installed! Staged files will be scanned for concurrency issues on every commit."
}

do_hooks_uninstall() {
  if ! git rev-parse --git-dir &>/dev/null 2>&1; then
    log_error "Not inside a git repository."
    return 1
  fi

  local repo_root
  repo_root=$(git rev-parse --show-toplevel)
  local config="$repo_root/lefthook.yml"

  if [[ -f "$config" ]]; then
    if grep -q "concurrencyguard" "$config" 2>/dev/null; then
      local tmp
      tmp=$(mktemp)
      sed '/# --- ConcurrencyGuard hooks/,/fail_text:.*$/d' "$config" > "$tmp"
      grep -v "concurrencyguard" "$tmp" > "$config" 2>/dev/null || mv "$tmp" "$config"
      rm -f "$tmp"
      echo -e "${GREEN}+${NC} Removed ConcurrencyGuard hooks"
    else
      log_warn "No ConcurrencyGuard hooks found"
    fi
  else
    log_warn "No lefthook.yml found"
  fi
}

# --- Pattern listing --------------------------------------------------------

do_list_patterns() {
  source "$SCRIPT_DIR/patterns.sh"

  echo -e "${BOLD}--- ConcurrencyGuard Detection Patterns ---${NC}"
  echo ""
  echo -e "${BOLD}$(concurrencyguard_pattern_count) patterns across 6 categories${NC}"
  echo ""

  local cat_types=("SS" "LK" "TC" "AW" "TS" "DL")
  local cat_labels=("Shared State" "Locking & Mutex" "TOCTOU & Atomicity" "Async/Await Pitfalls" "Thread Safety" "Deadlock & Starvation")

  for i in "${!cat_types[@]}"; do
    local ctype="${cat_types[$i]}"
    local clabel="${cat_labels[$i]}"
    echo -e "${CYAN}${BOLD}$clabel:${NC}"
    concurrencyguard_list_patterns "$ctype" | while IFS= read -r line; do
      echo "  $line"
    done
    echo ""
  done
}

# --- Command dispatch -------------------------------------------------------

main() {
  local cmd="${1:-}"
  shift || true

  case "$cmd" in
    scan)
      local target="${1:-.}"
      log_info "Scanning for concurrency issues in ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      do_concurrency_scan "$target" 5
      ;;

    hook)
      local subcmd="${1:-}"
      case "$subcmd" in
        install)
          do_hooks_install
          ;;
        uninstall)
          do_hooks_uninstall
          ;;
        *)
          log_error "Unknown hook subcommand: $subcmd"
          echo "  Usage: concurrencyguard hook [install|uninstall]"
          exit 1
          ;;
      esac
      ;;

    report)
      local target="${1:-.}"
      log_info "Generating concurrency safety report for ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      generate_concurrency_report "$target"
      ;;

    watch)
      require_license "pro"
      local target="${1:-.}"
      log_info "Starting watch mode on ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      watch_mode "$target"
      ;;

    ci)
      require_license "pro"
      local target="${1:-.}"
      log_info "Running CI mode on ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      ci_mode "$target"
      ;;

    team-report)
      require_license "team"
      local target="${1:-.}"
      log_info "Generating team report for ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      team_report "$target"
      ;;

    baseline)
      require_license "team"
      local target="${1:-.}"
      log_info "Creating baseline for ${BOLD}$target${NC}"
      source "$SCRIPT_DIR/patterns.sh"
      source "$SCRIPT_DIR/analyzer.sh"
      baseline_mode "$target"
      ;;

    patterns)
      do_list_patterns
      ;;

    status)
      show_version
      echo ""
      source "$SCRIPT_DIR/license.sh"
      show_concurrencyguard_status
      ;;

    --help|-h|help)
      show_help
      ;;

    --version|-v)
      show_version
      ;;

    "")
      show_help
      exit 1
      ;;

    *)
      log_error "Unknown command: $cmd"
      echo ""
      show_help
      exit 1
      ;;
  esac
}

main "$@"


path: scripts/license.sh
content: #!/usr/bin/env bash
# ConcurrencyGuard -- License Validation Module
# Same JWT-based offline validation as SecretScan/ConfigSafe/DocSync/DepGuard/EnvGuard/DeadCode

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

OPENCLAW_CONFIG="${HOME}/.openclaw/openclaw.json"
CONCURRENCYGUARD_LICENSE_KEY="${CONCURRENCYGUARD_LICENSE_KEY:-}"

declare -A TIER_LEVELS=(
  [free]=0
  [pro]=1
  [team]=2
  [enterprise]=3
)

get_concurrencyguard_key() {
  if [[ -n "$CONCURRENCYGUARD_LICENSE_KEY" ]]; then
    echo "$CONCURRENCYGUARD_LICENSE_KEY"
    return 0
  fi

  if [[ -f "$OPENCLAW_CONFIG" ]]; then
    local key=""
    if command -v python3 &>/dev/null; then
      key=$(python3 -c "
import json
try:
    with open('$OPENCLAW_CONFIG') as f:
        cfg = json.load(f)
    print(cfg.get('skills', {}).get('entries', {}).get('concurrencyguard', {}).get('apiKey', ''))
except: pass
" 2>/dev/null)
    elif command -v node &>/dev/null; then
      key=$(node -e "
try {
  const cfg = require('$OPENCLAW_CONFIG');
  console.log(cfg?.skills?.entries?.concurrencyguard?.apiKey || '');
} catch(e) {}
" 2>/dev/null)
    elif command -v jq &>/dev/null; then
      key=$(jq -r '.skills.entries.concurrencyguard.apiKey // ""' "$OPENCLAW_CONFIG" 2>/dev/null)
    fi

    if [[ -n "${key:-}" ]]; then
      echo "$key"
      return 0
    fi
  fi
  return 1
}

decode_jwt_payload() {
  local token="$1"

  # --- JWT Structure Validation ---
  # A valid JWT must have exactly 3 dot-separated parts: header.payload.signature
  local dot_count
  dot_count=$(echo "$token" | tr -cd '.' | wc -c)
  if [[ "$dot_count" -ne 2 ]]; then
    echo "[JWT] Malformed [REDACTED_SECRET] 3 parts (header.payload.signature), got $((dot_count + 1))" >&2
    return 1
  fi

  local header payload signature
  header=$(echo "$token" | cut -d. -f1)
  payload=$(echo "$token" | cut -d. -f2)
  signature=$(echo "$token" | cut -d. -f3)

  # Signature must be non-empty
  if [[ -z "$signature" ]]; then
    echo "[JWT] Rejected: signature segment is empty (unsigned token)" >&2
    return 1
  fi

  # --- Signature Verification (HMAC-SHA256 via openssl, if available) ---
  # NOTE: Full cryptographic verification requires the shared secret. When
  # CLAWHUB_JWT_SECRET is set, we verify the HMAC-SHA256 signature. Without
  # the secret, we still enforce structural integrity (3-part, non-empty sig)
  # which blocks trivially forged tokens (e.g., alg:none attacks).
  if [[ -n "${CLAWHUB_JWT_SECRET:-}" ]] && command -v openssl &>/dev/null; then
    local signing_input="${header}.${payload}"
    local expected_sig
    expected_sig=$(printf '%s' "$signing_input" \
      | openssl dgst -sha256 -hmac "$CLAWHUB_JWT_SECRET" -binary 2>/dev/null \
      | openssl base64 -e 2>/dev/null \
      | tr '/+' '_-' | tr -d '=')
    if [[ "$expected_sig" != "$signature" ]]; then
      echo "[JWT] Signature verification failed" >&2
      return 1
    fi
  fi

  # --- Decode Payload ---
  local padded="$payload"
  local mod=$((${#payload} % 4))
  if [[ $mod -eq 2 ]]; then padded="${payload}=="; fi
  if [[ $mod -eq 3 ]]; then padded="${payload}="; fi

  local decoded
  decoded=$(echo "$padded" | tr '_-' '/+' | base64 -d 2>/dev/null || \
            echo "$padded" | tr '_-' '/+' | base64 -D 2>/dev/null || \
            echo "$padded" | tr '_-' '/+' | openssl base64 -d 2>/dev/null) || {
    echo "[JWT] Failed to decode payload" >&2
    return 1
  }

  # --- Expiry Check ---
  # Check exp claim inline to catch expired tokens at decode time
  local exp_val=""
  if command -v python3 &>/dev/null; then
    exp_val=$(python3 -c "import json; print(json.loads('$decoded').get('exp', ''))" 2>/dev/null || true)
  elif command -v node &>/dev/null; then
    exp_val=$(node -e "console.log(JSON.parse('$decoded').exp || '')" 2>/dev/null || true)
  elif command -v jq &>/dev/null; then
    exp_val=$(echo "$decoded" | jq -r '.exp // ""' 2>/dev/null || true)
  fi

  if [[ -n "$exp_val" && "$exp_val" != "null" ]]; then
    local now
    now=$(date +%s)
    if [[ "$now" -gt "$exp_val" ]]; then
      echo "[JWT] Token expired (exp=$exp_val, now=$now)" >&2
      return 1
    fi
  fi

  echo "$decoded"
}

extract_field() {
  local json="$1" field="$2"
  if command -v python3 &>/dev/null; then
    python3 -c "import json; print(json.loads('$json').get('$field', ''))" 2>/dev/null
  elif command -v node &>/dev/null; then
    node -e "console.log(JSON.parse('$json')['$field'] || '')" 2>/dev/null
  elif command -v jq &>/dev/null; then
    echo "$json" | jq -r ".$field // \"\"" 2>/dev/null
  else
    echo "$json" | grep -oP "\"$field\"\s*:\s*\"?\K[^\",$}]+" 2>/dev/null || echo ""
  fi
}

validate_license() {
  local key="$1"
  local payload
  if ! payload=$(decode_jwt_payload "$key"); then
    return 1
  fi

  local product
  product=$(extract_field "$payload" "product")

  # Validate product matches
  if [[ "$product" != "concurrencyguard" && -n "$product" ]]; then
    return 1
  fi

  echo "$payload"
  return 0
}

check_concurrencyguard_license() {
  local required_tier="${1:-pro}"
  local required_level="${TIER_LEVELS[$required_tier]:-1}"

  local key
  if ! key=$(get_concurrencyguard_key) || [[ -z "$key" ]]; then
    echo -e "${RED}${BOLD}--- ConcurrencyGuard License Required ---${NC}"
    echo ""
    echo -e "This feature requires a ${BOLD}${required_tier}${NC} license."
    echo ""
    echo -e "Get your license at: ${CYAN}${BOLD}https://concurrencyguard.pages.dev/pricing${NC}"
    echo ""
    echo "Then configure it:"
    echo -e "  ${DIM}# In ~/.openclaw/openclaw.json:${NC}"
    echo -e "  ${CYAN}\"skills\": { \"entries\": { \"concurrencyguard\": { \"apiKey\": \"YOUR_KEY\" } } }${NC}"
    echo ""
    echo -e "  ${DIM}# Or set the environment variable:${NC}"
    echo -e "  ${CYAN}export CONCURRENCYGUARD_LICENSE_KEY=\"YOUR_KEY\"${NC}"
    return 1
  fi

  local payload
  if ! payload=$(decode_jwt_payload "$key"); then
    echo -e "${RED}[ConcurrencyGuard]${NC} Invalid license key."
    echo -e "Get a valid license at: ${CYAN}https://concurrencyguard.pages.dev/pricing${NC}"
    return 1
  fi

  local product tier expiry
  product=$(extract_field "$payload" "product")
  tier=$(extract_field "$payload" "tier")
  expiry=$(extract_field "$payload" "exp")

  # Validate product
  if [[ -n "$product" && "$product" != "concurrencyguard" ]]; then
    echo -e "${RED}[ConcurrencyGuard]${NC} License is for product '${BOLD}$product${NC}', not concurrencyguard."
    echo -e "Get a ConcurrencyGuard license at: ${CYAN}https://concurrencyguard.pages.dev/pricing${NC}"
    return 1
  fi

  local actual_level="${TIER_LEVELS[$tier]:-0}"
  if [[ "$actual_level" -lt "$required_level" ]]; then
    echo -e "${RED}[ConcurrencyGuard]${NC} This feature requires ${BOLD}$required_tier${NC} tier. You have: ${BOLD}$tier${NC}"
    echo -e "Upgrade at: ${CYAN}https://concurrencyguard.pages.dev/upgrade${NC}"
    return 1
  fi

  if [[ -n "$expiry" ]]; then
    local now
    now=$(date +%s)
    if [[ "$now" -gt "$expiry" ]]; then
      echo -e "${RED}[ConcurrencyGuard]${NC} License expired. Renew at: ${CYAN}https://concurrencyguard.pages.dev/renew${NC}"
      return 1
    fi
  fi

  return 0
}

get_concurrencyguard_tier() {
  local key
  if ! key=$(get_concurrencyguard_key) || [[ -z "$key" ]]; then
    echo "free"
    return 0
  fi

  local payload
  if ! payload=$(decode_jwt_payload "$key" 2>/dev/null); then
    echo "free"
    return 0
  fi

  local tier
  tier=$(extract_field "$payload" "tier")
  echo "${tier:-free}"
}

show_concurrencyguard_status() {
  local key
  if ! key=$(get_concurrencyguard_key) || [[ -z "$key" ]]; then
    echo -e "License: ${YELLOW}Free tier${NC}"
    echo -e "  Available: scan (5 file limit), hook, report"
    echo -e "  Upgrade at: ${CYAN}https://concurrencyguard.pages.dev/pricing${NC}"
    return
  fi

  local payload tier expiry seats product
  payload=$(decode_jwt_payload "$key" 2>/dev/null) || { echo -e "License: ${RED}Invalid key${NC}"; return; }
  product=$(extract_field "$payload" "product")
  tier=$(extract_field "$payload" "tier")
  expiry=$(extract_field "$payload" "exp")
  seats=$(extract_field "$payload" "seats")

  # Validate product
  if [[ -n "$product" && "$product" != "concurrencyguard" ]]; then
    echo -e "License: ${RED}Wrong product ($product)${NC}"
    echo -e "  This key is for $product, not ConcurrencyGuard."
    echo -e "  Get a ConcurrencyGuard license at: ${CYAN}https://concurrencyguard.pages.dev/pricing${NC}"
    return
  fi

  local now status_color status_text
  now=$(date +%s)
  status_color="$GREEN"; status_text="Active"
  if [[ -n "$expiry" && "$now" -gt "$expiry" ]]; then
    status_color="$RED"; status_text="Expired"
  fi

  local expiry_date="Never"
  [[ -n "$expiry" ]] && expiry_date=$(date -d "@$expiry" +%Y-%m-%d 2>/dev/null || date -r "$expiry" +%Y-%m-%d 2>/dev/null || echo "unknown")

  echo -e "License: ${status_color}${BOLD}${tier^^}${NC} (${status_color}${status_text}${NC})"
  echo -e "  Product: concurrencyguard"
  echo -e "  Seats: ${seats:-unlimited}"
  echo -e "  Expires: $expiry_date"
  echo ""

  case "$tier" in
    pro)
      echo -e "  Available: scan (unlimited), hook, report, watch, ci"
      echo -e "  Upgrade to Team for: team-report, baseline, custom policies"
      ;;
    team)
      echo -e "  Available: scan, hook, report, watch, ci, team-report, baseline"
      ;;
    enterprise)
      echo -e "  Available: All features + priority support"
      ;;
  esac
}


path: scripts/patterns.sh
content: #!/usr/bin/env bash
# ConcurrencyGuard -- Concurrency Safety Pattern Definitions
# Each pattern: REGEX|SEVERITY|CHECK_ID|DESCRIPTION|RECOMMENDATION
#
# Severity levels:
#   critical -- Race conditions that can cause data corruption or security issues
#   high     -- Concurrency hazards likely to cause intermittent bugs
#   medium   -- Potential concurrency issues, may be false positives
#   low      -- Informational, style issues, missing annotations
#
# IMPORTANT: All regexes must use POSIX ERE syntax (grep -E compatible).
# - Use [[:space:]] instead of \s
# - Use [[:alnum:]] instead of \w
# - Avoid Perl-only features (\d, \w, etc.)

set -euo pipefail

# ============================================================================
# 1. SHARED STATE (SS-001 through SS-015)
# ============================================================================

declare -a CONCURRENCYGUARD_SS_PATTERNS=()

CONCURRENCYGUARD_SS_PATTERNS+=(
  # --- SS-001: Global mutable variable in Go accessed without sync ---
  'var[[:space:]]+[[:alnum:]_]+[[:space:]]+(map|(\[\])|chan[[:space:]])|critical|SS-001|Global mutable variable (map/slice/chan) declared at package level in Go|Protect with sync.Mutex or sync.RWMutex, or use sync.Map for concurrent map access'

  # --- SS-002: Unprotected static mutable field in Java ---
  'static[[:space:]]+(private|public|protected|volatile)?[[:space:]]*[[:alpha:]]+[[:space:]]+[[:alnum:]_]+[[:space:]]*=[[:space:]]*new[[:space:]]|critical|SS-002|Mutable static field initialized with new object in Java (not final)|Make field final or protect all access with synchronized blocks'

  # --- SS-003: Module-level mutable state in Python ---
  '^[[:alpha:]_]+[[:space:]]*=[[:space:]]*(\{|\[|dict\(|list\(|set\(|collections\.|defaultdict)|high|SS-003|Module-level mutable data structure in Python (shared across threads)|Use threading.local() or pass as parameter; protect with threading.Lock()'

  # --- SS-004: Shared variable without volatile in Java ---
  'private[[:space:]]+static[[:space:]]+[[:alpha:]]+[[:space:]]+[[:alnum:]_]+[[:space:]]*;|high|SS-004|Non-volatile static field in Java (may not be visible across threads)|Add volatile keyword or use AtomicReference/AtomicInteger for thread visibility'

  # --- SS-005: Global object mutation in Node worker threads ---
  'workerData|parentPort|SharedArrayBuffer|Atomics|high|SS-005|Node.js worker thread shared memory usage detected|Use Atomics for SharedArrayBuffer access; avoid sharing mutable state between workers'

  # --- SS-006: Mutable class variable in Python ---
  'class[[:space:]]+[[:alnum:]_]+.*:[[:space:]]*$|high|SS-006|Python class with potential shared mutable class variables|Use instance variables (self.x) instead of class variables for mutable state'

  # --- SS-007: Static collection modified without sync in Java ---
  'static[[:space:]]+(final[[:space:]]+)?[[:alpha:]]*[[:space:]]*(List|Map|Set|Queue|Deque|Collection)[[:space:]]*<|high|SS-007|Static collection field in Java (potential concurrent modification)|Use ConcurrentHashMap, CopyOnWriteArrayList, or Collections.synchronized* wrappers'

  # --- SS-008: Unprotected global counter ---
  '(count|total|counter|num_|idx|index)[[:space:]]*(\+\+|\+=|-=|--)|medium|SS-008|Global counter modified with non-atomic increment/decrement|Use atomic operations (AtomicInteger, sync/atomic, Interlocked) or protect with mutex'

  # --- SS-009: Shared dict without concurrent variant in Python ---
  'threading\.(Thread|Lock|RLock)|high|SS-009|Threading module usage detected -- verify all shared state is protected|Audit all module-level and class-level mutable variables for proper synchronization'

  # --- SS-010: Go global modified in goroutine ---
  'go[[:space:]]+func[[:space:]]*\(|critical|SS-010|Goroutine launched with potential closure over shared variable|Capture variables by value in goroutine closure, or protect shared state with sync.Mutex'

  # --- SS-011: Rust unsafe mutable static ---
  'static[[:space:]]+mut[[:space:]]|critical|SS-011|Unsafe mutable static in Rust (data race risk)|Use std::sync::Mutex, std::sync::RwLock, or std::sync::atomic types instead'

  # --- SS-012: Module-level list append in Python ---
  '^[[:alpha:]_]+\.(append|extend|insert|pop|remove|update|clear)\(|medium|SS-012|Module-level collection mutation in Python|Protect with threading.Lock() if accessed from multiple threads'

  # --- SS-013: Shared buffer modification ---
  '(shared|global|static)[[:space:]]*[[:alpha:]]*[[:space:]]*(buf|buffer|data|cache)[[:space:]]*[=\[]|high|SS-013|Shared buffer or data array without explicit synchronization|Use synchronized access, concurrent data structures, or immutable buffers'

  # --- SS-014: Global config modified at runtime ---
  '(config|settings|options|params)\.(set|update|modify|put|remove|delete|clear)\(|medium|SS-014|Global configuration object modified at runtime|Make configuration immutable after initialization; use copy-on-write for updates'

  # --- SS-015: C# static field without lock ---
  'private[[:space:]]+static[[:space:]]+[[:alpha:]]+[[:space:]]+_[[:alnum:]]+[[:space:]]*=|high|SS-015|C# private static field (potential shared mutable state)|Protect with lock statement, use Interlocked class, or make readonly'
)

# ============================================================================
# 2. LOCKING & MUTEX (LK-001 through LK-015)
# ============================================================================

declare -a CONCURRENCYGUARD_LK_PATTERNS=()

CONCURRENCYGUARD_LK_PATTERNS+=(
  # --- LK-001: Missing synchronized on shared method in Java ---
  'public[[:space:]]+static[[:space:]]+void[[:space:]]+[[:alnum:]_]+[[:space:]]*\([^)]*\)[[:space:]]*\{|critical|LK-001|Public static method without synchronized keyword in Java|Add synchronized keyword or use explicit ReentrantLock for thread-safe access'

  # --- LK-002: Missing lock in C# ---
  'static[[:space:]]+[[:alpha:]]+[[:space:]]+[[:alnum:]_]+[[:space:]]*\([^)]*\)[[:space:]]*\{|high|LK-002|Static method in C# without lock protection for shared state|Use lock(lockObj) statement or Interlocked class for atomic operations'

  # --- LK-003: Missing mutex.Lock in Go ---
  '\.(Lock|RLock)\(\)|high|LK-003|Mutex Lock() call detected -- verify matching Unlock() exists|Ensure every Lock() has a corresponding defer Unlock() immediately after'

  # --- LK-004: Lock without defer Unlock in Go ---
  '\.Lock\(\)[[:space:]]*$|critical|LK-004|Mutex Lock() without defer Unlock() on same line or next line in Go|Always use defer mu.Unlock() immediately after mu.Lock() to prevent missed unlocks'

  # --- LK-005: Nested lock acquisition ---
  'synchronized[[:space:]]*\([[:alnum:]_]+\)[[:space:]]*\{[^}]*synchronized[[:space:]]*\(|high|LK-005|Nested synchronized blocks in Java (deadlock risk)|Use a single lock or establish consistent lock ordering; consider java.util.concurrent'

  # --- LK-006: Missing RWMutex for read-heavy data in Go ---
  'sync\.Mutex|high|LK-006|Using sync.Mutex -- consider sync.RWMutex if reads greatly outnumber writes|Use sync.RWMutex with RLock()/RUnlock() for read operations to improve throughput'

  # --- LK-007: Spin lock in user code ---
  'while[[:space:]]*\([[:space:]]*(true|1|![[:space:]]*done|![[:space:]]*ready|![[:space:]]*flag)|medium|LK-007|Busy-wait spin loop detected (CPU waste, potential livelock)|Use proper synchronization: condition variables, semaphores, or channels'

  # --- LK-008: Lock not deferred in Go ---
  '([[:alnum:]_]+)\.Lock\(\)[[:space:]]*\n[^d]|high|LK-008|Go mutex Lock() not followed by defer Unlock()|Always defer Unlock() immediately after Lock() to ensure unlock on panic'

  # --- LK-009: Missing threading.Lock in Python ---
  'import[[:space:]]+threading|high|LK-009|Python threading module imported -- verify Lock usage for shared state|Use threading.Lock() or threading.RLock() to protect all shared mutable state'

  # --- LK-010: Conditional unlock ---
  'if[[:space:]]*\(.*\)[[:space:]]*\{[^}]*(\.Unlock|\.unlock|Release|Monitor\.Exit)|critical|LK-010|Lock unlock inside conditional block (may not execute on all paths)|Move unlock outside conditional; use try-finally or defer for guaranteed unlock'

  # --- LK-011: Using Lock where RLock appropriate ---
  'threading\.Lock\(\)|medium|LK-011|Python threading.Lock used -- consider RLock if re-entrant locking needed|Use threading.RLock() if the same thread may need to acquire the lock multiple times'

  # --- LK-012: Mutex not held during condvar wait ---
  '(Condition|cond)\.(wait|Wait)\(|high|LK-012|Condition variable wait detected -- verify mutex is held during wait|Always hold the associated mutex/lock when calling wait() on a condition variable'

  # --- LK-013: Missing lock in C# property ---
  'public[[:space:]]+(static[[:space:]]+)?[[:alpha:]]+[[:space:]]+[[:alnum:]_]+[[:space:]]*\{[[:space:]]*get[[:space:]]*;[[:space:]]*set[[:space:]]*;|high|LK-013|Auto-property in C# without lock protection (if shared across threads)|Use lock statement in custom getter/setter, or use Interlocked for primitive types'

  # --- LK-014: Manual lock implementation ---
  '(while|for)[[:space:]]*\(.*\.(compareAndSet|compareAndSwap|CompareExchange)\(|medium|LK-014|Manual CAS-based lock implementation detected|Prefer language-provided lock primitives (synchronized, lock, sync.Mutex) over hand-rolled locks'

  # --- LK-015: Sync.Mutex copied by value in Go ---
  'func[[:space:]]+[[:alnum:]_]+\([[:space:]]*[[:alnum:]_]+[[:space:]]+[[:alnum:]_]*Mutex|high|LK-015|Go sync.Mutex or struct containing Mutex passed by value (copy risk)|Always pass Mutex by pointer; embed with pointer receiver methods'
)

# ============================================================================
# 3. TOCTOU & ATOMICITY (TC-001 through TC-015)
# ============================================================================

declare -a CONCURRENCYGUARD_TC_PATTERNS=()

CONCURRENCYGUARD_TC_PATTERNS+=(
  # --- TC-001: Check-then-act without sync ---
  'if[[:space:]]*\(.*\.(contains|has|get|exists|ContainsKey|TryGetValue|containsKey)\(|critical|TC-001|Check-then-act pattern on shared collection (TOCTOU risk)|Use atomic putIfAbsent/computeIfAbsent, or protect the check-and-act with a lock'

  # --- TC-002: File exists then open (TOCTOU) ---
  '(os\.path\.exists|File\.Exists|Files\.exists|os\.Stat|fs\.existsSync|Path\.exists)\(|critical|TC-002|File existence check followed by file operation (TOCTOU vulnerability)|Use atomic file operations: open with O_CREAT|O_EXCL, or use lockfile/flock'

  # --- TC-003: Read-modify-write without CAS ---
  '([[:alnum:]_]+)[[:space:]]*=[[:space:]]*\1[[:space:]]*[\+\-\*\/]|critical|TC-003|Read-modify-write on variable (x = x + n) without atomic operation|Use atomic increment (AtomicInteger, sync/atomic, Interlocked.Add) or protect with lock'

  # --- TC-004: Double-checked locking without volatile ---
  'if[[:space:]]*\([[:space:]]*[[:alnum:]_]+[[:space:]]*==[[:space:]]*null|high|TC-004|Null-check pattern (possible double-checked locking without volatile)|Ensure field is volatile (Java) or use Lazy<T> (C#) or sync.Once (Go) for safe lazy init'

  # --- TC-005: Non-atomic counter increment ---
  '\+\+[[:space:]]*;|\+\+[[:space:]]*$|[[:alnum:]_]+\+\+|critical|TC-005|Increment operator (++) which is not atomic on shared variable|Use AtomicInteger.incrementAndGet(), atomic.AddInt64(), or Interlocked.Increment()'

  # --- TC-006: Compare-and-swap needed ---
  'if[[:space:]]*\(.*==[[:space:]]*[[:alnum:]_]+\)[[:space:]]*\{[^}]*=[[:space:]]|high|TC-006|Compare-then-assign pattern without atomic CAS operation|Use compareAndSet/CompareExchange or protect with synchronized/lock'

  # --- TC-007: Get-then-put on Map ---
  '\.(get|Get)\([^)]+\)[[:space:]]*;[^;]*\.(put|Put|set|Set)\(|critical|TC-007|Get-then-put on Map/Dictionary without atomic operation|Use putIfAbsent, computeIfAbsent, ConcurrentDictionary.AddOrUpdate, or lock'

  # --- TC-008: Test-and-set without atomic ---
  'if[[:space:]]*\([[:space:]]*![[:alnum:]_]+(Done|Ready|Started|Initialized|Running)\)|high|TC-008|Boolean flag test-and-set without atomic operation|Use AtomicBoolean, atomic.Bool, or protect flag check-and-set with lock'

  # --- TC-009: Non-atomic boolean flag ---
  '(volatile|static)?[[:space:]]*(boolean|bool)[[:space:]]+[[:alnum:]_]*(flag|done|ready|running|started|stopped|cancelled)|critical|TC-009|Boolean flag used for thread communication (non-atomic risk)|Use AtomicBoolean (Java), atomic.Bool (Go), volatile (C#), or threading.Event (Python)'

  # --- TC-010: Lazy init without sync ---
  'if[[:space:]]*\([[:space:]]*[[:alnum:]_]+[[:space:]]*==[[:space:]]*(null|nil|None|undefined)\)[[:space:]]*\{|critical|TC-010|Lazy initialization with null check (thread-unsafe without synchronization)|Use sync.Once (Go), Lazy<T> (C#), double-checked locking with volatile, or @synchronized'

  # --- TC-011: Balance check then debit ---
  'if[[:space:]]*\(.*\.(balance|amount|quantity|stock|inventory|count)[[:space:]]*(>|>=|<|<=)|critical|TC-011|Balance/quantity check before modification (lost update risk)|Use database transaction with SELECT FOR UPDATE or optimistic locking with version field'

  # --- TC-012: Collection size then index ---
  '\.(size|length|count|Count)\(\)[[:space:]]*(>|>=|!=)[[:space:]]*0|high|TC-012|Collection size check before index access (TOCTOU on concurrent collection)|Use atomic operations on concurrent collections; lock around size-check-and-access'

  # --- TC-013: Non-atomic file write ---
  '(open|fopen|File\.Open|os\.OpenFile)\([^)]*["'"'"'](w|write)|medium|TC-013|File opened in write mode (non-atomic write risk for concurrent access)|Use atomic write pattern: write to temp file, then rename; or use file locks'

  # --- TC-014: DB read-check-write without FOR UPDATE ---
  'SELECT[[:space:]]+.*FROM[[:space:]]+.*WHERE|high|TC-014|Database SELECT without FOR UPDATE (lost update risk in concurrent transactions)|Use SELECT ... FOR UPDATE or optimistic locking with version column'

  # --- TC-015: Non-atomic multi-field update ---
  '\.(set[A-Z]|Set[A-Z])[[:alnum:]_]*\([^)]+\)[[:space:]]*;[^;]*\.(set[A-Z]|Set[A-Z])|critical|TC-015|Multiple setter calls without atomic publication (torn read risk)|Publish state atomically: use immutable objects, builder pattern, or protect with lock'
)

# ============================================================================
# 4. ASYNC/AWAIT PITFALLS (AW-001 through AW-015)
# ============================================================================

declare -a CONCURRENCYGUARD_AW_PATTERNS=()

CONCURRENCYGUARD_AW_PATTERNS+=(
  # --- AW-001: Await inside loop ---
  '(for|while|forEach)[[:space:]]*\(.*\)[[:space:]]*\{[^}]*await[[:space:]]|high|AW-001|Await inside loop executes sequentially instead of in parallel|Use Promise.all() or Promise.allSettled() with Array.map() for parallel execution'

  # --- AW-002: Missing await on async call ---
  '[[:alnum:]_]+\.(then|catch)\(|critical|AW-002|Promise used with .then()/.catch() -- may indicate missing await|Use await keyword instead of .then() chains for clearer error handling and flow control'

  # --- AW-003: Async void method in C# ---
  'async[[:space:]]+void[[:space:]]+[[:alnum:]_]+|high|AW-003|Async void method in C# (unobserved exceptions, cannot be awaited)|Use async Task instead of async void; only use async void for event handlers'

  # --- AW-004: Fire-and-forget promise ---
  '[[:alnum:]_]+\([^)]*\)[[:space:]]*;[[:space:]]*(//|/\*|$)|critical|AW-004|Potential fire-and-forget async call (no await, no .catch)|Add await keyword or attach .catch() handler to prevent unhandled rejections'

  # --- AW-005: State update after await ---
  'await[[:space:]]+[^;]+;[[:space:]]*[[:alnum:]_]+[[:space:]]*[+\-*/]?=[[:space:]]|high|AW-005|Shared state modification after await point (race condition risk)|Re-read state after await, use atomic operations, or protect with async mutex'

  # --- AW-006: Missing Promise.all ---
  'const[[:space:]]+[[:alnum:]_]+[[:space:]]*=[[:space:]]*await[[:space:]]+[[:alnum:]_]+\([^)]*\)[[:space:]]*;[[:space:]]*const[[:space:]]+[[:alnum:]_]+[[:space:]]*=[[:space:]]*await|high|AW-006|Sequential await calls that could run in parallel|Use Promise.all([fn1(), fn2()]) or Promise.allSettled() for independent operations'

  # --- AW-007: Async function without try/catch ---
  'async[[:space:]]+function[[:space:]]+[[:alnum:]_]+|medium|AW-007|Async function detected -- verify proper error handling exists|Add try/catch block or ensure caller handles rejected promise'

  # --- AW-008: Shared state across await boundary ---
  'this\.[[:alnum:]_]+[[:space:]]*=[[:space:]].*await|high|AW-008|Instance property mutation across await boundary (stale state risk)|Re-read this.prop after await; use local variable to capture state before await'

  # --- AW-009: Multiple awaits on same promise ---
  '(await[[:space:]]+[[:alnum:]_]+)[[:space:]]*;[^;]*(await[[:space:]]+[[:alnum:]_]+)|critical|AW-009|Same variable potentially awaited multiple times (double resolution)|Await a promise only once; store result in a variable for reuse'

  # --- AW-010: Callback mixed with async ---
  'callback\(|cb\(|next\(|done\(|medium|AW-010|Callback-based pattern mixed with async/await code|Convert callbacks to promises (util.promisify) or use fully async patterns'

  # --- AW-011: Missing error propagation ---
  '\.catch\([[:space:]]*\(\)[[:space:]]*=>[[:space:]]*\{[[:space:]]*\}|medium|AW-011|Empty catch handler swallowing async errors|Log the error or re-throw; empty catch blocks hide failures silently'

  # --- AW-012: Async generator without cleanup ---
  'async[[:space:]]+\*[[:space:]]*[[:alnum:]_]+|high|AW-012|Async generator function detected -- verify proper cleanup on early exit|Use try-finally in generator to clean up resources when consumer stops iteration'

  # --- AW-013: Unhandled promise rejection ---
  'new[[:space:]]+Promise[[:space:]]*\([[:space:]]*\([[:space:]]*(resolve|reject)|critical|AW-013|Promise constructor detected -- verify rejection is always handled|Always provide reject handler or use .catch(); unhandled rejections crash Node.js'

  # --- AW-014: Sequential async that could be parallel ---
  'await[[:space:]]+fetch\(|await[[:space:]]+axios|await[[:space:]]+http|medium|AW-014|Sequential HTTP await calls that may be parallelizable|Group independent HTTP calls with Promise.all() for better performance'

  # --- AW-015: Lock held across await ---
  '(lock|Lock|acquire)[[:space:]]*\([^)]*\)[[:space:]]*;[^;]*await[[:space:]]|high|AW-015|Lock or mutex held across an await point (may cause async deadlock)|Release lock before await, use async-aware mutex, or restructure to minimize lock scope'
)

# ============================================================================
# 5. THREAD SAFETY (TS-001 through TS-015)
# ============================================================================

declare -a CONCURRENCYGUARD_TS_PATTERNS=()

CONCURRENCYGUARD_TS_PATTERNS+=(
  # --- TS-001: Thread-unsafe singleton ---
  'private[[:space:]]+static[[:space:]]+[[:alnum:]_]+[[:space:]]+instance[[:space:]]*;|critical|TS-001|Thread-unsafe singleton pattern (unprotected static instance field)|Use enum singleton (Java), Lazy<T> (C#), sync.Once (Go), or volatile + double-checked locking'

  # --- TS-002: HashMap without sync in Java ---
  'new[[:space:]]+HashMap[[:space:]]*<|critical|TS-002|HashMap created without synchronization (not thread-safe)|Use ConcurrentHashMap or Collections.synchronizedMap() for concurrent access'

  # --- TS-003: Non-thread-safe SimpleDateFormat ---
  'new[[:space:]]+SimpleDateFormat[[:space:]]*\(|high|TS-003|SimpleDateFormat is not thread-safe in Java|Use DateTimeFormatter (Java 8+) which is thread-safe, or use ThreadLocal<SimpleDateFormat>'

  # --- TS-004: Mutable default in Python ---
  'def[[:space:]]+[[:alnum:]_]+\([^)]*=[[:space:]]*(\[|\{|dict\(|list\(|set\()|high|TS-004|Mutable default argument in Python function (shared across calls)|Use None as default and create new mutable inside function: if arg is None: arg = []'

  # --- TS-005: Lazy init without double-check ---
  'if[[:space:]]*\([[:space:]]*instance[[:space:]]*==[[:space:]]*null[[:space:]]*\)[[:space:]]*\{[^}]*instance[[:space:]]*=[[:space:]]*new|critical|TS-005|Lazy initialization without double-checked locking (race condition)|Use double-checked locking with volatile, Lazy<T>, or synchronized lazy init'

  # --- TS-006: StringBuilder shared across threads ---
  'static[[:space:]]+(final[[:space:]]+)?StringBuilder[[:space:]]|high|TS-006|Static StringBuilder in Java/C# (not thread-safe)|Use local StringBuilder per thread, or StringBuffer (synchronized) if sharing required'

  # --- TS-007: Shared regex object ---
  'static[[:space:]]+(final[[:space:]]+)?Pattern[[:space:]]|high|TS-007|Static Pattern/Regex object shared across threads (Matcher is not thread-safe)|Pattern is thread-safe but Matcher is not; create new Matcher per thread'

  # --- TS-008: Thread-unsafe cache ---
  '(Cache|cache)[[:space:]]*=[[:space:]]*new[[:space:]]+(HashMap|Dictionary|dict|Map)\b|critical|TS-008|Cache implemented with non-concurrent data structure|Use ConcurrentHashMap, ConcurrentDictionary, or a thread-safe cache library'

  # --- TS-009: Shared Random instance ---
  'static[[:space:]]+(final[[:space:]]+)?Random[[:space:]]|high|TS-009|Shared static Random instance (contention, poor randomness under concurrency)|Use ThreadLocalRandom (Java), or create per-thread Random instances'

  # --- TS-010: Mutable return from synchronized ---
  'synchronized[[:space:]]*.*return[[:space:]]+[[:alnum:]_]+[[:space:]]*;|medium|TS-010|Returning mutable reference from synchronized method (escaping lock)|Return defensive copy or immutable view of the data'

  # --- TS-011: Thread-unsafe event handler ---
  '(addEventListener|on\([[:space:]]*["'"'"']|\.on[A-Z][[:alnum:]_]*[[:space:]]*=)|high|TS-011|Event handler registration without synchronization|Use thread-safe event bus or synchronize handler list modifications'

  # --- TS-012: Publishing this in constructor ---
  'this\.[[:alnum:]_]+[[:space:]]*=[[:space:]]*this[[:space:]]*;|critical|TS-012|Publishing this reference during construction (object may be incomplete)|Never leak this reference during construction; use factory method pattern'

  # --- TS-013: Iteration over shared collection ---
  'for[[:space:]]*\([[:alnum:][:space:]_]+:[[:space:]]*[[:alnum:]_]+\)|high|TS-013|Enhanced for-loop over potentially shared collection (ConcurrentModificationException)|Use iterator from ConcurrentHashMap, copy collection before iteration, or sync'

  # --- TS-014: Thread-unsafe collection usage ---
  'new[[:space:]]+(ArrayList|LinkedList|HashSet|TreeSet|TreeMap|LinkedHashMap|PriorityQueue)[[:space:]]*[<(]|medium|TS-014|Non-thread-safe collection created (verify not shared across threads)|Use concurrent equivalents: CopyOnWriteArrayList, ConcurrentSkipListSet, etc.'

  # --- TS-015: Missing @ThreadSafe annotation ---
  '@(Singleton|Service|Component|Repository|Controller)[[:space:]]*$|high|TS-015|Spring/Jakarta bean without @ThreadSafe annotation (shared by default)|Add @ThreadSafe annotation and ensure all fields are immutable or properly synchronized'
)

# ============================================================================
# 6. DEADLOCK & STARVATION (DL-001 through DL-015)
# ============================================================================

declare -a CONCURRENCYGUARD_DL_PATTERNS=()

CONCURRENCYGUARD_DL_PATTERNS+=(
  # --- DL-001: Inconsistent lock ordering ---
  'synchronized[[:space:]]*\([[:space:]]*[[:alnum:]_]+[[:space:]]*\).*synchronized[[:space:]]*\([[:space:]]*[[:alnum:]_]+[[:space:]]*\)|critical|DL-001|Multiple synchronized blocks with potentially inconsistent lock ordering|Always acquire locks in a consistent global order; use Lock.tryLock() with timeout'

  # --- DL-002: Lock held while calling external ---
  'synchronized[[:space:]]*\([^)]*\)[[:space:]]*\{[^}]*\.[[:alnum:]_]+\([^)]*\)|high|DL-002|Calling external/virtual method while holding lock (deadlock risk)|Release lock before calling external methods; use open call pattern'

  # --- DL-003: Go channel without timeout ---
  '<-[[:space:]]*[[:alnum:]_]+[[:space:]]*$|critical|DL-003|Blocking channel receive without timeout in Go (potential goroutine leak)|Use select with time.After or context.Done for timeout on channel operations'

  # --- DL-004: Unbuffered channel in goroutine ---
  'make\(chan[[:space:]]+[[:alnum:]_]+\)[[:space:]]*$|high|DL-004|Unbuffered channel created (sender blocks until receiver ready)|Use buffered channel make(chan T, n) or ensure goroutine pairs are properly managed'

  # --- DL-005: Missing select default ---
  'select[[:space:]]*\{[^}]*case[^}]*\}|high|DL-005|Select statement without default case (may block indefinitely)|Add default case for non-blocking operation, or use time.After for timeout'

  # --- DL-006: Lock across await point ---
  '(lock|Lock|synchronized|mutex)\([^)]*\)[[:space:]]*\{[^}]*await[[:space:]]|critical|DL-006|Lock/mutex held across await point (async deadlock risk)|Release lock before await, or use async-compatible lock (SemaphoreSlim in C#)'

  # --- DL-007: Recursive lock acquisition ---
  'synchronized[[:space:]]*\([[:space:]]*this[[:space:]]*\).*synchronized[[:space:]]*\([[:space:]]*this[[:space:]]*\)|high|DL-007|Recursive synchronized on same monitor (reentrant but fragile)|Use explicit ReentrantLock or restructure to avoid nested locking on same object'

  # --- DL-008: Multiple locks without ordering ---
  '(lock|Lock)\([[:space:]]*[[:alnum:]_]+[[:space:]]*\)[[:space:]]*;[^;]*(lock|Lock)\([[:space:]]*[[:alnum:]_]+[[:space:]]*\)|critical|DL-008|Acquiring multiple locks without consistent ordering (deadlock risk)|Define and enforce a global lock ordering; use java.util.concurrent.locks.Lock.tryLock()'

  # --- DL-009: Blocking call inside synchronized ---
  'synchronized[[:space:]]*\([^)]*\)[[:space:]]*\{[^}]*(Thread\.sleep|wait\(\)|\.get\(\)|\.join\(\)|\.await\(\))|high|DL-009|Blocking call inside synchronized block (starvation and deadlock risk)|Move blocking operations outside the synchronized block; use condition variables'

  # --- DL-010: Thread.sleep inside lock ---
  '(Thread\.sleep|time\.sleep|Task\.Delay|time\.Sleep)\(|medium|DL-010|Thread sleep call detected -- verify not inside a lock or synchronized block|Never sleep while holding a lock; use condition variables or timed waits instead'

  # --- DL-011: Missing timeout on lock acquisition ---
  '\.lock\(\)[[:space:]]*;|\.Lock\(\)|high|DL-011|Lock acquisition without timeout (may deadlock indefinitely)|Use tryLock(timeout) in Java, Monitor.TryEnter in C#, or select with timeout in Go'

  # --- DL-012: Goroutine without context ---
  'go[[:space:]]+[[:alnum:]_]+\(|critical|DL-012|Goroutine spawned without context.Context (potential goroutine leak)|Pass context.Context and select on ctx.Done() for cancellation support'

  # --- DL-013: WaitGroup.Add inside goroutine ---
  'go[[:space:]]+func.*wg\.Add\(|high|DL-013|WaitGroup.Add() called inside goroutine (race with wg.Wait())|Call wg.Add() BEFORE launching the goroutine, not inside it'

  # --- DL-014: Reader-writer with writer starvation ---
  'RWMutex|ReentrantReadWriteLock|ReadWriteLock|medium|DL-014|Reader-writer lock detected -- verify writers are not starved|Monitor for writer starvation; consider fair read-write lock or write-preferring lock'

  # --- DL-015: Semaphore without timeout ---
  '(Semaphore|semaphore)\.(acquire|Acquire|Wait|wait)\(|high|DL-015|Semaphore acquisition without timeout|Use tryAcquire(timeout) or WaitOne(timeout) to prevent indefinite blocking'
)

# ============================================================================
# Utility Functions
# ============================================================================

# Get total pattern count across all categories
concurrencyguard_pattern_count() {
  local count=0
  count=$((count + ${#CONCURRENCYGUARD_SS_PATTERNS[@]}))
  count=$((count + ${#CONCURRENCYGUARD_LK_PATTERNS[@]}))
  count=$((count + ${#CONCURRENCYGUARD_TC_PATTERNS[@]}))
  count=$((count + ${#CONCURRENCYGUARD_AW_PATTERNS[@]}))
  count=$((count + ${#CONCURRENCYGUARD_TS_PATTERNS[@]}))
  count=$((count + ${#CONCURRENCYGUARD_DL_PATTERNS[@]}))
  echo "$count"
}

# List patterns by category
concurrencyguard_list_patterns() {
  local filter_type="${1:-all}"
  local -n _cg_patterns_ref

  case "$filter_type" in
    SS) _cg_patterns_ref=CONCURRENCYGUARD_SS_PATTERNS ;;
    LK) _cg_patterns_ref=CONCURRENCYGUARD_LK_PATTERNS ;;
    TC) _cg_patterns_ref=CONCURRENCYGUARD_TC_PATTERNS ;;
    AW) _cg_patterns_ref=CONCURRENCYGUARD_AW_PATTERNS ;;
    TS) _cg_patterns_ref=CONCURRENCYGUARD_TS_PATTERNS ;;
    DL) _cg_patterns_ref=CONCURRENCYGUARD_DL_PATTERNS ;;
    all)
      concurrencyguard_list_patterns "SS"
      concurrencyguard_list_patterns "LK"
      concurrencyguard_list_patterns "TC"
      concurrencyguard_list_patterns "AW"
      concurrencyguard_list_patterns "TS"
      concurrencyguard_list_patterns "DL"
      return
      ;;
    *)
      echo "Unknown category: $filter_type"
      return 1
      ;;
  esac

  for entry in "${_cg_patterns_ref[@]}"; do
    IFS='|' read -r regex severity check_id description recommendation <<< "$entry"
    printf "%-12s %-8s %s\n" "$check_id" "$severity" "$description"
  done
}

# Get patterns array name for a category
get_concurrencyguard_patterns_for_category() {
  local category="$1"
  case "$category" in
    ss) echo "CONCURRENCYGUARD_SS_PATTERNS" ;;
    lk) echo "CONCURRENCYGUARD_LK_PATTERNS" ;;
    tc) echo "CONCURRENCYGUARD_TC_PATTERNS" ;;
    aw) echo "CONCURRENCYGUARD_AW_PATTERNS" ;;
    ts) echo "CONCURRENCYGUARD_TS_PATTERNS" ;;
    dl) echo "CONCURRENCYGUARD_DL_PATTERNS" ;;
    *)  echo "" ;;
  esac
}

# All category names for iteration
get_all_concurrencyguard_categories() {
  echo "ss lk tc aw ts dl"
}

# Severity to numeric points for scoring
severity_to_points() {
  case "$1" in
    critical) echo 25 ;;
    high)     echo 15 ;;
    medium)   echo 8 ;;
    low)      echo 3 ;;
    *)        echo 0 ;;
  esac
}


path: scripts/scan.sh
content: #!/usr/bin/env bash
# scan.sh — Minimal wrapper that sources patterns and runs the scan
# Generated by generate-skills.sh — do not edit manually
#
# Usage: bash scan.sh [file|directory]
#   Defaults to scanning current directory if no argument given.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Source patterns for concurrencyguard
source "$SCRIPT_DIR/patterns.sh"

# Delegate to main entry point
exec bash "$SCRIPT_DIR/concurrencyguard.sh" scan "${@:-.}"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)