Files
lijiaoqiao/tools/reviewer_tool.py
Your Name c169d35c72 feat: add reviewer tool, lazy loading, manifest profiles, instinct learning, and hook profiles
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)
2026-04-14 22:40:56 +08:00

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="🔍",
)