Initial commit
This commit is contained in:
410
skills/adw-bootstrap/utils/validator.py
Normal file
410
skills/adw-bootstrap/utils/validator.py
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validation utilities for ADW setup.
|
||||
|
||||
Simple checks to ensure ADW infrastructure is correctly installed and functional.
|
||||
These are mechanical validations that don't require AI reasoning.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
def check_claude_installed() -> Tuple[bool, str]:
|
||||
"""Check if Claude Code CLI is available.
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
return (True, f"✓ Claude Code CLI installed: {version}")
|
||||
else:
|
||||
return (False, "✗ Claude Code CLI not responding correctly")
|
||||
|
||||
except FileNotFoundError:
|
||||
return (False, "✗ Claude Code CLI not found. Install from: https://claude.ai/code")
|
||||
except subprocess.TimeoutExpired:
|
||||
return (False, "✗ Claude Code CLI timeout")
|
||||
except Exception as e:
|
||||
return (False, f"✗ Error checking Claude Code: {e}")
|
||||
|
||||
|
||||
def check_api_key_configured() -> Tuple[bool, str]:
|
||||
"""Check if ANTHROPIC_API_KEY is configured (optional).
|
||||
|
||||
Returns:
|
||||
Tuple of (configured, message)
|
||||
"""
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
|
||||
if api_key:
|
||||
# Mask the key for display
|
||||
masked = api_key[:10] + "..." + api_key[-4:] if len(api_key) > 14 else "***"
|
||||
return (True, f"✓ API key configured: {masked}")
|
||||
else:
|
||||
return (False, "ℹ No API key (using subscription mode)")
|
||||
|
||||
|
||||
def validate_directory_structure(project_root: str) -> List[str]:
|
||||
"""Check that expected ADW directories exist.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
|
||||
Returns:
|
||||
List of validation messages
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
messages = []
|
||||
|
||||
# Required directories
|
||||
required_dirs = [
|
||||
"adws",
|
||||
"adws/adw_modules",
|
||||
".claude/commands",
|
||||
"specs"
|
||||
]
|
||||
|
||||
for dir_path in required_dirs:
|
||||
full_path = project_path / dir_path
|
||||
if full_path.exists() and full_path.is_dir():
|
||||
messages.append(f"✓ Directory exists: {dir_path}")
|
||||
else:
|
||||
messages.append(f"✗ Directory missing: {dir_path}")
|
||||
|
||||
# Optional but expected directories
|
||||
optional_dirs = [
|
||||
"agents",
|
||||
]
|
||||
|
||||
for dir_path in optional_dirs:
|
||||
full_path = project_path / dir_path
|
||||
if full_path.exists() and full_path.is_dir():
|
||||
messages.append(f"✓ Directory exists: {dir_path}")
|
||||
else:
|
||||
messages.append(f"ℹ Directory not created yet: {dir_path} (will be created on first run)")
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def validate_core_files(project_root: str) -> List[str]:
|
||||
"""Check that core ADW files exist.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
|
||||
Returns:
|
||||
List of validation messages
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
messages = []
|
||||
|
||||
# Core files for minimal setup
|
||||
core_files = [
|
||||
"adws/adw_modules/agent.py",
|
||||
"adws/adw_prompt.py",
|
||||
".claude/commands/chore.md",
|
||||
".claude/commands/implement.md",
|
||||
]
|
||||
|
||||
for file_path in core_files:
|
||||
full_path = project_path / file_path
|
||||
if full_path.exists() and full_path.is_file():
|
||||
messages.append(f"✓ File exists: {file_path}")
|
||||
else:
|
||||
messages.append(f"✗ File missing: {file_path}")
|
||||
|
||||
# Enhanced setup files (optional)
|
||||
enhanced_files = [
|
||||
"adws/adw_modules/agent_sdk.py",
|
||||
"adws/adw_slash_command.py",
|
||||
"adws/adw_chore_implement.py",
|
||||
".claude/commands/feature.md",
|
||||
]
|
||||
|
||||
enhanced_count = sum(1 for f in enhanced_files if (project_path / f).exists())
|
||||
if enhanced_count > 0:
|
||||
messages.append(f"ℹ Enhanced setup detected ({enhanced_count}/{len(enhanced_files)} files)")
|
||||
for file_path in enhanced_files:
|
||||
full_path = project_path / file_path
|
||||
if full_path.exists() and full_path.is_file():
|
||||
messages.append(f" ✓ {file_path}")
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def check_scripts_executable(project_root: str) -> List[str]:
|
||||
"""Verify ADW scripts have execute permissions.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
|
||||
Returns:
|
||||
List of validation messages
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
messages = []
|
||||
|
||||
# Find all adw_*.py scripts
|
||||
adws_dir = project_path / "adws"
|
||||
if not adws_dir.exists():
|
||||
return ["✗ adws/ directory not found"]
|
||||
|
||||
scripts = list(adws_dir.glob("adw_*.py"))
|
||||
|
||||
if not scripts:
|
||||
messages.append("ℹ No ADW scripts found (adw_*.py)")
|
||||
return messages
|
||||
|
||||
for script in scripts:
|
||||
is_executable = os.access(script, os.X_OK)
|
||||
if is_executable:
|
||||
messages.append(f"✓ Executable: {script.name}")
|
||||
else:
|
||||
messages.append(f"⚠ Not executable: {script.name} (run: chmod +x {script})")
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def test_prompt_execution(project_root: str, timeout: int = 30) -> Tuple[bool, str]:
|
||||
"""Try a simple prompt execution to verify setup works.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
timeout: Maximum seconds to wait
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
prompt_script = project_path / "adws" / "adw_prompt.py"
|
||||
|
||||
if not prompt_script.exists():
|
||||
return (False, "✗ adw_prompt.py not found")
|
||||
|
||||
try:
|
||||
# Simple test prompt
|
||||
result = subprocess.run(
|
||||
[str(prompt_script), "What is 2 + 2?"],
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return (True, "✓ Test prompt executed successfully")
|
||||
else:
|
||||
error_msg = result.stderr[:200] if result.stderr else "Unknown error"
|
||||
return (False, f"✗ Test prompt failed: {error_msg}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return (False, f"✗ Test prompt timeout (>{timeout}s)")
|
||||
except Exception as e:
|
||||
return (False, f"✗ Error executing test prompt: {e}")
|
||||
|
||||
|
||||
def validate_output_structure(project_root: str) -> List[str]:
|
||||
"""Check if output directories are created correctly after execution.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
|
||||
Returns:
|
||||
List of validation messages
|
||||
"""
|
||||
project_path = Path(project_root)
|
||||
agents_dir = project_path / "agents"
|
||||
|
||||
if not agents_dir.exists():
|
||||
return ["ℹ No agents/ directory yet (created on first execution)"]
|
||||
|
||||
messages = [f"✓ Output directory exists: agents/"]
|
||||
|
||||
# Check for any execution directories
|
||||
execution_dirs = [d for d in agents_dir.iterdir() if d.is_dir()]
|
||||
|
||||
if not execution_dirs:
|
||||
messages.append("ℹ No execution outputs yet")
|
||||
return messages
|
||||
|
||||
messages.append(f"ℹ Found {len(execution_dirs)} execution output(s)")
|
||||
|
||||
# Check structure of first execution
|
||||
first_exec = execution_dirs[0]
|
||||
agent_dirs = [d for d in first_exec.iterdir() if d.is_dir()]
|
||||
|
||||
if agent_dirs:
|
||||
first_agent = agent_dirs[0]
|
||||
expected_files = [
|
||||
"cc_raw_output.jsonl",
|
||||
"cc_raw_output.json",
|
||||
"cc_final_object.json",
|
||||
"custom_summary_output.json"
|
||||
]
|
||||
|
||||
for filename in expected_files:
|
||||
if (first_agent / filename).exists():
|
||||
messages.append(f" ✓ Output file: {filename}")
|
||||
else:
|
||||
messages.append(f" ⚠ Missing output: {filename}")
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def run_full_validation(project_root: str = ".", verbose: bool = True) -> bool:
|
||||
"""Run complete validation suite.
|
||||
|
||||
Args:
|
||||
project_root: Path to project root
|
||||
verbose: Print detailed messages
|
||||
|
||||
Returns:
|
||||
True if all critical checks pass
|
||||
"""
|
||||
project_root = os.path.abspath(project_root)
|
||||
|
||||
if verbose:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"ADW Setup Validation")
|
||||
print(f"{'='*60}")
|
||||
print(f"Project: {project_root}\n")
|
||||
|
||||
all_passed = True
|
||||
|
||||
# Check 1: Claude Code CLI
|
||||
if verbose:
|
||||
print("1. Claude Code CLI")
|
||||
print("-" * 60)
|
||||
|
||||
success, msg = check_claude_installed()
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
if not success:
|
||||
all_passed = False
|
||||
|
||||
# Check 2: API Key (optional)
|
||||
if verbose:
|
||||
print("\n2. API Configuration")
|
||||
print("-" * 60)
|
||||
|
||||
configured, msg = check_api_key_configured()
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
|
||||
# Check 3: Directory structure
|
||||
if verbose:
|
||||
print("\n3. Directory Structure")
|
||||
print("-" * 60)
|
||||
|
||||
dir_messages = validate_directory_structure(project_root)
|
||||
for msg in dir_messages:
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
if msg.startswith("✗"):
|
||||
all_passed = False
|
||||
|
||||
# Check 4: Core files
|
||||
if verbose:
|
||||
print("\n4. Core Files")
|
||||
print("-" * 60)
|
||||
|
||||
file_messages = validate_core_files(project_root)
|
||||
for msg in file_messages:
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
if msg.startswith("✗"):
|
||||
all_passed = False
|
||||
|
||||
# Check 5: Script permissions
|
||||
if verbose:
|
||||
print("\n5. Script Permissions")
|
||||
print("-" * 60)
|
||||
|
||||
perm_messages = check_scripts_executable(project_root)
|
||||
for msg in perm_messages:
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
if msg.startswith("⚠"):
|
||||
# Warning but not critical failure
|
||||
pass
|
||||
|
||||
# Check 6: Output structure (informational)
|
||||
if verbose:
|
||||
print("\n6. Output Structure")
|
||||
print("-" * 60)
|
||||
|
||||
output_messages = validate_output_structure(project_root)
|
||||
for msg in output_messages:
|
||||
if verbose:
|
||||
print(f" {msg}")
|
||||
|
||||
# Summary
|
||||
if verbose:
|
||||
print(f"\n{'='*60}")
|
||||
if all_passed:
|
||||
print("✓ Validation passed - ADW setup is ready!")
|
||||
else:
|
||||
print("✗ Validation failed - see errors above")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return all_passed
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point for validation."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate ADW setup",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"project_root",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Project root directory (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Only show summary"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test",
|
||||
action="store_true",
|
||||
help="Run test prompt execution (may take 30s)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Run validation
|
||||
passed = run_full_validation(args.project_root, verbose=not args.quiet)
|
||||
|
||||
# Optionally test execution
|
||||
if args.test and passed:
|
||||
print("\nRunning test prompt execution...")
|
||||
print("-" * 60)
|
||||
success, msg = test_prompt_execution(args.project_root)
|
||||
print(f" {msg}")
|
||||
if not success:
|
||||
passed = False
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user