411 lines
11 KiB
Python
411 lines
11 KiB
Python
#!/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()
|