Features: - reviewer_tool: Multi-language code review (Python, JS/TS, Go, Rust, Java, C++, C#, PHP) - lazy_loader: Context-triggered skill loading with 3-tier budget system - manifest: Profile-based skill installation (coding, debugging, planning, shipping, research) - instinct: Learning system that tracks patterns and predicts skill loading - hook_profile: Security hook profiles (minimal, standard, strict, developer, rust-dev) - hook_tool + hooks: Post-write hook execution system Code Review Tool: - Supports 9 languages with lint, typecheck, format, test, security checks - Auto-detects language from file extensions - Configurable tools per language (e.g., ruff, eslint, golangci-lint) Lazy Loading: - CONTEXT_TO_SKILLS mapping for 20+ context triggers - Budget-aware loading (Tier 0: core, Tier 1: context, Tier 2: rare) - Emergency mode at >90% context usage Integration: - Registered in model_tools.py and toolsets.py - 8 language reviewers mapped in lazy_loader (python, go, rust, js, java, cpp, csharp, php)
685 lines
20 KiB
Python
685 lines
20 KiB
Python
"""
|
|
Code Reviewer Tool
|
|
|
|
Runs code quality checks for various languages.
|
|
Automatically detects language and runs appropriate linters, type checkers, and tests.
|
|
|
|
Usage:
|
|
from tools.reviewer_tool import run_code_review
|
|
|
|
result = run_code_review(
|
|
language="python",
|
|
paths=["src/"],
|
|
check_types=["lint", "typecheck", "test"]
|
|
)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# Language Configurations
|
|
# =============================================================================
|
|
|
|
LANGUAGE_CONFIG: Dict[str, Dict[str, Any]] = {
|
|
"python": {
|
|
"extensions": [".py"],
|
|
"lint": {
|
|
"commands": [
|
|
("ruff", ["ruff", "check", "."]),
|
|
("flake8", ["flake8", "."]),
|
|
],
|
|
"default": "ruff",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("mypy", ["mypy", "."]),
|
|
],
|
|
"default": "mypy",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("ruff format", ["ruff", "format", "--check", "."]),
|
|
("black", ["black", "--check", "."]),
|
|
("isort", ["isort", "--check-only", "."]),
|
|
],
|
|
"default": "ruff format",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("pytest", ["pytest", "-v", "--tb=short"]),
|
|
("pytest coverage", ["pytest", "--cov=.", "--cov-report=term-missing"]),
|
|
],
|
|
"default": "pytest",
|
|
},
|
|
"security": {
|
|
"commands": [
|
|
("bandit", ["bandit", "-r", "."]),
|
|
("safety", ["safety", "check"]),
|
|
],
|
|
"default": "bandit",
|
|
},
|
|
},
|
|
"javascript": {
|
|
"extensions": [".js", ".jsx", ".mjs"],
|
|
"lint": {
|
|
"commands": [
|
|
("eslint", ["npx", "eslint", "src/"]),
|
|
("eslint fix", ["npx", "eslint", "src/", "--fix"]),
|
|
],
|
|
"default": "eslint",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("tsc", ["npx", "tsc", "--noEmit"]),
|
|
],
|
|
"default": "tsc",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("prettier", ["npx", "prettier", "--check", "src/"]),
|
|
],
|
|
"default": "prettier",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("jest", ["npm", "test"]),
|
|
("vitest", ["npx", "vitest", "run"]),
|
|
],
|
|
"default": "jest",
|
|
},
|
|
},
|
|
"typescript": {
|
|
"extensions": [".ts", ".tsx"],
|
|
"lint": {
|
|
"commands": [
|
|
("eslint", ["npx", "eslint", "src/", "--ext", ".ts,.tsx"]),
|
|
],
|
|
"default": "eslint",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("tsc", ["npx", "tsc", "--noEmit", "--project", "tsconfig.json"]),
|
|
],
|
|
"default": "tsc",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("prettier", ["npx", "prettier", "--check", "src/"]),
|
|
],
|
|
"default": "prettier",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("jest", ["npx", "jest"]),
|
|
("vitest", ["npx", "vitest", "run"]),
|
|
],
|
|
"default": "jest",
|
|
},
|
|
},
|
|
"go": {
|
|
"extensions": [".go"],
|
|
"lint": {
|
|
"commands": [
|
|
("golangci-lint", ["golangci-lint", "run", "./..."]),
|
|
("gofmt", ["gofmt", "-d", "."]),
|
|
],
|
|
"default": "golangci-lint",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("go vet", ["go", "vet", "./..."]),
|
|
],
|
|
"default": "go vet",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("gofmt", ["gofmt", "-d", "."]),
|
|
("go fmt", ["go", "fmt", "./..."]),
|
|
],
|
|
"default": "gofmt",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("go test", ["go", "test", "-v", "./..."]),
|
|
("go test coverage", ["go", "test", "-coverprofile=coverage.out", "./..."]),
|
|
],
|
|
"default": "go test",
|
|
},
|
|
},
|
|
"rust": {
|
|
"extensions": [".rs"],
|
|
"lint": {
|
|
"commands": [
|
|
("clippy", ["cargo", "clippy", "--", "-D", "warnings"]),
|
|
],
|
|
"default": "clippy",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("cargo check", ["cargo", "check"]),
|
|
],
|
|
"default": "cargo check",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("cargo fmt", ["cargo", "fmt", "--", "--check"]),
|
|
],
|
|
"default": "cargo fmt",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("cargo test", ["cargo", "test", "--", "--nocapture"]),
|
|
],
|
|
"default": "cargo test",
|
|
},
|
|
},
|
|
"java": {
|
|
"extensions": [".java"],
|
|
"lint": {
|
|
"commands": [
|
|
("checkstyle", ["mvn", "checkstyle:check"]),
|
|
],
|
|
"default": "checkstyle",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("mvn compile", ["mvn", "compile"]),
|
|
],
|
|
"default": "mvn compile",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("mvn formatter", ["mvn", "formatter:format"]),
|
|
],
|
|
"default": "mvn formatter",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("mvn test", ["mvn", "test"]),
|
|
],
|
|
"default": "mvn test",
|
|
},
|
|
},
|
|
"cpp": {
|
|
"extensions": [".cpp", ".hpp", ".h", ".cc"],
|
|
"lint": {
|
|
"commands": [
|
|
("clang-tidy", ["clang-tidy", "src/**"]),
|
|
("cppcheck", ["cppcheck", "--enable=all", "src/"]),
|
|
],
|
|
"default": "clang-tidy",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("cmake build", ["cmake", "--build", "build", "--target", "all"]),
|
|
],
|
|
"default": "cmake build",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("clang-format", ["clang-format", "-i", "src/**"]),
|
|
],
|
|
"default": "clang-format",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("ctest", ["ctest", "--output-on-failure"]),
|
|
],
|
|
"default": "ctest",
|
|
},
|
|
},
|
|
"csharp": {
|
|
"extensions": [".cs"],
|
|
"lint": {
|
|
"commands": [
|
|
("dotnet format", ["dotnet", "format", "--verify-no-changes"]),
|
|
],
|
|
"default": "dotnet format",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("dotnet build", ["dotnet", "build", "--no-restore"]),
|
|
],
|
|
"default": "dotnet build",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("dotnet format", ["dotnet", "format", "--verify-no-changes"]),
|
|
],
|
|
"default": "dotnet format",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("dotnet test", ["dotnet", "test", "--no-build"]),
|
|
],
|
|
"default": "dotnet test",
|
|
},
|
|
},
|
|
"php": {
|
|
"extensions": [".php"],
|
|
"lint": {
|
|
"commands": [
|
|
("phpcs", ["./vendor/bin/phpcs", "--standard=PSR12", "src/"]),
|
|
("phpstan", ["./vendor/bin/phpstan", "analyse", "src/", "--level=max"]),
|
|
],
|
|
"default": "phpcs",
|
|
},
|
|
"typecheck": {
|
|
"commands": [
|
|
("php -l", ["php", "-l", "src/"]),
|
|
],
|
|
"default": "php -l",
|
|
},
|
|
"format": {
|
|
"commands": [
|
|
("phpcbf", ["./vendor/bin/phpcbf", "--standard=PSR12", "src/"]),
|
|
],
|
|
"default": "phpcbf",
|
|
},
|
|
"test": {
|
|
"commands": [
|
|
("phpunit", ["./vendor/bin/phpunit"]),
|
|
("pest", ["./vendor/bin/pest"]),
|
|
],
|
|
"default": "phpunit",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Data Structures
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class CheckResult:
|
|
"""Result of a single check."""
|
|
name: str
|
|
success: bool
|
|
command: str
|
|
output: str
|
|
duration_ms: float
|
|
error_count: int = 0
|
|
warning_count: int = 0
|
|
|
|
|
|
@dataclass
|
|
class ReviewResult:
|
|
"""Result of a full code review."""
|
|
language: str
|
|
paths: List[str]
|
|
success: bool
|
|
checks: List[CheckResult]
|
|
total_duration_ms: float
|
|
summary: str
|
|
blocked: bool = False # True if CRITICAL issues found
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"language": self.language,
|
|
"paths": self.paths,
|
|
"success": self.success,
|
|
"blocked": self.blocked,
|
|
"total_duration_ms": self.total_duration_ms,
|
|
"summary": self.summary,
|
|
"checks": [
|
|
{
|
|
"name": c.name,
|
|
"success": c.success,
|
|
"command": c.command,
|
|
"error_count": c.error_count,
|
|
"warning_count": c.warning_count,
|
|
"output": c.output[:500] if len(c.output) > 500 else c.output,
|
|
}
|
|
for c in self.checks
|
|
],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Core Functions
|
|
# =============================================================================
|
|
|
|
def detect_language(paths: List[str]) -> Optional[str]:
|
|
"""Detect language from file extensions."""
|
|
extension_map: Dict[str, str] = {}
|
|
for lang, config in LANGUAGE_CONFIG.items():
|
|
for ext in config.get("extensions", []):
|
|
extension_map[ext] = lang
|
|
|
|
detected = set()
|
|
for path in paths:
|
|
p = Path(path)
|
|
if p.is_file():
|
|
ext = p.suffix
|
|
if ext in extension_map:
|
|
detected.add(extension_map[ext])
|
|
elif p.is_dir():
|
|
for ext, lang in extension_map.items():
|
|
if list(p.rglob(f"*{ext}")):
|
|
detected.add(lang)
|
|
|
|
if len(detected) == 1:
|
|
return list(detected)[0]
|
|
elif len(detected) > 1:
|
|
# Prefer more specific languages
|
|
priority = ["typescript", "javascript", "python", "rust", "go", "java", "cpp", "csharp", "php"]
|
|
for lang in priority:
|
|
if lang in detected:
|
|
return lang
|
|
return None
|
|
|
|
|
|
def run_command(cmd: List[str], timeout: int = 120) -> Tuple[int, str, str]:
|
|
"""Run a command and return (returncode, stdout, stderr)."""
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
cwd=os.getcwd(),
|
|
)
|
|
return result.returncode, result.stdout, result.stderr
|
|
except subprocess.TimeoutExpired:
|
|
return -1, "", "Command timed out"
|
|
except FileNotFoundError:
|
|
return -2, "", f"Command not found: {cmd[0]}"
|
|
except Exception as e:
|
|
return -3, "", str(e)
|
|
|
|
|
|
def parse_check_output(name: str, output: str, returncode: int) -> Tuple[bool, int, int]:
|
|
"""Parse check output to determine success and counts."""
|
|
if returncode == -2:
|
|
# Command not found - not an error
|
|
return True, 0, 0
|
|
|
|
if returncode == 0:
|
|
return True, 0, 0
|
|
|
|
# Try to parse error/warning counts from output
|
|
error_count = 0
|
|
warning_count = 0
|
|
|
|
# Common patterns
|
|
output_lower = output.lower()
|
|
|
|
if "error" in output_lower:
|
|
import re
|
|
errors = re.findall(r'\b(\d+)\s+error', output_lower)
|
|
if errors:
|
|
error_count = sum(int(e) for e in errors)
|
|
|
|
if "warning" in output_lower:
|
|
warnings = re.findall(r'\b(\d+)\s+warning', output_lower)
|
|
if warnings:
|
|
warning_count = sum(int(w) for w in warnings)
|
|
|
|
success = returncode == 0
|
|
return success, error_count, warning_count
|
|
|
|
|
|
def run_check(
|
|
language: str,
|
|
check_type: str,
|
|
paths: List[str],
|
|
tool_name: Optional[str] = None,
|
|
) -> CheckResult:
|
|
"""Run a single check for a language."""
|
|
import time
|
|
|
|
if language not in LANGUAGE_CONFIG:
|
|
return CheckResult(
|
|
name=f"{check_type}",
|
|
success=False,
|
|
command="",
|
|
output=f"Unknown language: {language}",
|
|
duration_ms=0,
|
|
)
|
|
|
|
config = LANGUAGE_CONFIG[language]
|
|
check_config = config.get(check_type)
|
|
|
|
if not check_config:
|
|
return CheckResult(
|
|
name=f"{check_type}",
|
|
success=True,
|
|
command="",
|
|
output=f"No {check_type} configured for {language}",
|
|
duration_ms=0,
|
|
)
|
|
|
|
commands = check_config.get("commands", [])
|
|
if not commands:
|
|
return CheckResult(
|
|
name=f"{check_type}",
|
|
success=True,
|
|
command="",
|
|
output=f"No commands for {check_type}",
|
|
duration_ms=0,
|
|
)
|
|
|
|
# Find the requested tool or use default
|
|
if tool_name:
|
|
cmd_list = [c for c in commands if c[0] == tool_name]
|
|
if not cmd_list:
|
|
return CheckResult(
|
|
name=f"{check_type}",
|
|
success=False,
|
|
command="",
|
|
output=f"Tool {tool_name} not found for {check_type}",
|
|
duration_ms=0,
|
|
)
|
|
cmd = cmd_list[0]
|
|
else:
|
|
# Use default tool
|
|
default_name = check_config.get("default", commands[0][0])
|
|
cmd = next((c for c in commands if c[0] == default_name), commands[0])
|
|
|
|
# Expand paths
|
|
expanded_cmd = cmd[1]
|
|
if paths:
|
|
# Add paths to command if it doesn't already have them
|
|
if "." in expanded_cmd or any("$" not in c for c in expanded_cmd):
|
|
expanded_cmd = expanded_cmd + paths
|
|
|
|
start_time = time.time()
|
|
returncode, stdout, stderr = run_command(expanded_cmd)
|
|
duration_ms = (time.time() - start_time) * 1000
|
|
|
|
output = stdout if stdout else stderr
|
|
success, error_count, warning_count = parse_check_output(cmd[0], output, returncode)
|
|
|
|
return CheckResult(
|
|
name=f"{check_type}:{cmd[0]}",
|
|
success=success,
|
|
command=" ".join(expanded_cmd),
|
|
output=output,
|
|
duration_ms=duration_ms,
|
|
error_count=error_count,
|
|
warning_count=warning_count,
|
|
)
|
|
|
|
|
|
def run_code_review(
|
|
language: Optional[str] = None,
|
|
paths: Optional[List[str]] = None,
|
|
check_types: Optional[List[str]] = None,
|
|
tools: Optional[Dict[str, str]] = None,
|
|
) -> ReviewResult:
|
|
"""
|
|
Run a full code review.
|
|
|
|
Args:
|
|
language: Language to check (auto-detected if not provided)
|
|
paths: Files/directories to check (defaults to current directory)
|
|
check_types: Types of checks to run (defaults to lint, typecheck)
|
|
tools: Override specific tools, e.g., {"lint": "flake8", "test": "pytest"}
|
|
|
|
Returns:
|
|
ReviewResult with all check results
|
|
"""
|
|
import time
|
|
|
|
if paths is None:
|
|
paths = ["."]
|
|
|
|
if language is None:
|
|
language = detect_language(paths) or "unknown"
|
|
|
|
if check_types is None:
|
|
check_types = ["lint", "typecheck"]
|
|
|
|
if tools is None:
|
|
tools = {}
|
|
|
|
start_time = time.time()
|
|
checks = []
|
|
total_errors = 0
|
|
total_warnings = 0
|
|
|
|
for check_type in check_types:
|
|
tool_name = tools.get(check_type)
|
|
result = run_check(language, check_type, paths, tool_name)
|
|
checks.append(result)
|
|
total_errors += result.error_count
|
|
total_warnings += result.warning_count
|
|
|
|
total_duration_ms = (time.time() - start_time) * 1000
|
|
|
|
# Determine overall success and blocked status
|
|
all_passed = all(c.success for c in checks)
|
|
has_critical = total_errors > 0
|
|
|
|
summary = f"{len(checks)} checks, {total_errors} errors, {total_warnings} warnings"
|
|
|
|
return ReviewResult(
|
|
language=language,
|
|
paths=paths,
|
|
success=all_passed,
|
|
checks=checks,
|
|
total_duration_ms=total_duration_ms,
|
|
summary=summary,
|
|
blocked=has_critical and check_types == ["lint"],
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Tool Entry Point
|
|
# =============================================================================
|
|
|
|
def code_review_tool(
|
|
language: str = None,
|
|
paths: str = None,
|
|
check_types: str = None,
|
|
tools: str = None,
|
|
) -> str:
|
|
"""
|
|
Run code quality checks for a language.
|
|
|
|
Args:
|
|
language: Language to check (python, javascript, typescript, go, rust, java, cpp, csharp, php)
|
|
paths: Comma-separated list of files/directories to check (default: current directory)
|
|
check_types: Comma-separated check types (lint, typecheck, format, test, security)
|
|
tools: JSON string mapping check types to specific tools, e.g., '{"lint": "ruff"}'
|
|
|
|
Returns:
|
|
JSON string with ReviewResult
|
|
"""
|
|
# Parse inputs
|
|
lang = language.lower() if language else None
|
|
path_list = [p.strip() for p in paths.split(",")] if paths else ["."]
|
|
check_list = [c.strip() for c in check_types.split(",")] if check_types else ["lint", "typecheck"]
|
|
tool_map = json.loads(tools) if tools else {}
|
|
|
|
# Run review
|
|
result = run_code_review(
|
|
language=lang,
|
|
paths=path_list,
|
|
check_types=check_list,
|
|
tools=tool_map,
|
|
)
|
|
|
|
return json.dumps(result.to_dict(), indent=2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python reviewer_tool.py <language> [paths] [check_types]")
|
|
sys.exit(1)
|
|
|
|
language = sys.argv[1]
|
|
paths = sys.argv[2:3] if len(sys.argv) > 2 else ["."]
|
|
check_types = sys.argv[3:4] if len(sys.argv) > 3 else ["lint"]
|
|
|
|
result = run_code_review(
|
|
language=language,
|
|
paths=paths,
|
|
check_types=check_types,
|
|
)
|
|
|
|
print(json.dumps(result.to_dict(), indent=2))
|
|
|
|
|
|
# =============================================================================
|
|
# Registry Registration
|
|
# =============================================================================
|
|
|
|
from tools.registry import registry
|
|
|
|
registry.register(
|
|
name="code_review",
|
|
toolset="reviewer",
|
|
schema={
|
|
"name": "code_review",
|
|
"description": "Run code quality checks (lint, typecheck, test) for a specific language. "
|
|
"Supports Python, JavaScript, TypeScript, Go, Rust, Java, C++, C#, PHP. "
|
|
"Returns structured JSON with check results, error counts, and blocked status.",
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"language": {
|
|
"type": "string",
|
|
"description": "Language to check (python, javascript, typescript, go, rust, java, cpp, csharp, php). "
|
|
"Auto-detected if not provided.",
|
|
},
|
|
"paths": {
|
|
"type": "string",
|
|
"description": "Comma-separated list of files/directories to check. Defaults to current directory.",
|
|
},
|
|
"check_types": {
|
|
"type": "string",
|
|
"description": "Comma-separated check types: lint, typecheck, format, test, security. "
|
|
"Defaults to 'lint,typecheck'.",
|
|
},
|
|
"tools": {
|
|
"type": "string",
|
|
"description": "JSON string mapping check types to specific tools, e.g., '{\"lint\": \"ruff\", \"test\": \"pytest\"}'",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
handler=lambda args, **kw: code_review_tool(
|
|
language=args.get("language"),
|
|
paths=args.get("paths"),
|
|
check_types=args.get("check_types"),
|
|
tools=args.get("tools"),
|
|
),
|
|
check_fn=lambda: True, # Always available
|
|
requires_env=[],
|
|
description="Run code quality checks for a language",
|
|
emoji="🔍",
|
|
)
|