Files
Aperant/auto-claude/runners/roadmap/phases.py
T
2025-12-16 21:57:45 +01:00

331 lines
12 KiB
Python

"""
Core phases for roadmap generation.
"""
import json
import shutil
from pathlib import Path
from typing import TYPE_CHECKING
from debug import (
debug,
debug_detailed,
debug_error,
debug_success,
debug_warning,
)
from ui import print_status
from .models import RoadmapPhaseResult
if TYPE_CHECKING:
from .executor import AgentExecutor, ScriptExecutor
MAX_RETRIES = 3
class ProjectIndexPhase:
"""Handles project index creation and validation."""
def __init__(
self,
output_dir: Path,
refresh: bool,
script_executor: "ScriptExecutor",
):
self.output_dir = output_dir
self.refresh = refresh
self.script_executor = script_executor
self.project_index = output_dir / "project_index.json"
self.auto_build_index = Path(__file__).parent.parent / "project_index.json"
async def execute(self) -> RoadmapPhaseResult:
"""Ensure project index exists."""
debug("roadmap_phase", "Starting phase: project_index")
debug_detailed(
"roadmap_phase",
"Checking for existing project index",
project_index=str(self.project_index),
auto_build_index=str(self.auto_build_index),
)
# Check if we can copy existing index
if self.auto_build_index.exists() and not self.project_index.exists():
debug(
"roadmap_phase", "Copying existing project_index.json from auto-claude"
)
shutil.copy(self.auto_build_index, self.project_index)
print_status("Copied existing project_index.json", "success")
debug_success("roadmap_phase", "Project index copied successfully")
return RoadmapPhaseResult(
"project_index", True, [str(self.project_index)], [], 0
)
if self.project_index.exists() and not self.refresh:
debug("roadmap_phase", "project_index.json already exists, skipping")
print_status("project_index.json already exists", "success")
return RoadmapPhaseResult(
"project_index", True, [str(self.project_index)], [], 0
)
# Run analyzer
debug("roadmap_phase", "Running project analyzer to create index")
print_status("Running project analyzer...", "progress")
success, output = self.script_executor.run_script(
"analyzer.py", ["--output", str(self.project_index)]
)
if success and self.project_index.exists():
debug_success("roadmap_phase", "Created project_index.json")
print_status("Created project_index.json", "success")
return RoadmapPhaseResult(
"project_index", True, [str(self.project_index)], [], 0
)
debug_error(
"roadmap_phase",
"Failed to create project index",
output=output[:500] if output else None,
)
return RoadmapPhaseResult("project_index", False, [], [output], 1)
class DiscoveryPhase:
"""Handles project discovery and audience understanding."""
def __init__(
self,
output_dir: Path,
refresh: bool,
agent_executor: "AgentExecutor",
):
self.output_dir = output_dir
self.refresh = refresh
self.agent_executor = agent_executor
self.discovery_file = output_dir / "roadmap_discovery.json"
self.project_index_file = output_dir / "project_index.json"
async def execute(self) -> RoadmapPhaseResult:
"""Run discovery phase to understand project and audience."""
debug("roadmap_phase", "Starting phase: discovery")
if self.discovery_file.exists() and not self.refresh:
debug("roadmap_phase", "roadmap_discovery.json already exists, skipping")
print_status("roadmap_discovery.json already exists", "success")
return RoadmapPhaseResult(
"discovery", True, [str(self.discovery_file)], [], 0
)
errors = []
for attempt in range(MAX_RETRIES):
debug("roadmap_phase", f"Discovery attempt {attempt + 1}/{MAX_RETRIES}")
print_status(
f"Running discovery agent (attempt {attempt + 1})...", "progress"
)
context = self._build_context()
success, output = await self.agent_executor.run_agent(
"roadmap_discovery.md",
additional_context=context,
)
if success and self.discovery_file.exists():
validation_result = self._validate_discovery(attempt)
if validation_result is not None:
return validation_result
errors.append(f"Validation failed on attempt {attempt + 1}")
else:
debug_warning(
"roadmap_phase",
f"Discovery attempt {attempt + 1} failed - file not created",
)
errors.append(
f"Attempt {attempt + 1}: Agent did not create discovery file"
)
debug_error(
"roadmap_phase", "Discovery phase failed after all retries", errors=errors
)
return RoadmapPhaseResult("discovery", False, [], errors, MAX_RETRIES)
def _build_context(self) -> str:
"""Build context string for the discovery agent."""
return f"""
**Project Index**: {self.project_index_file}
**Output Directory**: {self.output_dir}
**Output File**: {self.discovery_file}
IMPORTANT: This runs NON-INTERACTIVELY. Do NOT ask questions or wait for user input.
Your task:
1. Analyze the project (read README, code structure, git history)
2. Infer target audience, vision, and constraints from your analysis
3. IMMEDIATELY create {self.discovery_file} with your findings
Do NOT ask questions. Make educated inferences and create the file.
"""
def _validate_discovery(self, attempt: int) -> RoadmapPhaseResult | None:
"""Validate the discovery file.
Returns RoadmapPhaseResult if validation succeeds, None otherwise.
"""
try:
with open(self.discovery_file) as f:
data = json.load(f)
required = ["project_name", "target_audience", "product_vision"]
missing = [k for k in required if k not in data]
if not missing:
debug_success(
"roadmap_phase",
"Created valid roadmap_discovery.json",
attempt=attempt + 1,
)
print_status("Created valid roadmap_discovery.json", "success")
return RoadmapPhaseResult(
"discovery", True, [str(self.discovery_file)], [], attempt
)
else:
debug_warning("roadmap_phase", f"Missing required fields: {missing}")
return None
except json.JSONDecodeError as e:
debug_error("roadmap_phase", "Invalid JSON in discovery file", error=str(e))
return None
class FeaturesPhase:
"""Handles feature generation and prioritization."""
def __init__(
self,
output_dir: Path,
refresh: bool,
agent_executor: "AgentExecutor",
):
self.output_dir = output_dir
self.refresh = refresh
self.agent_executor = agent_executor
self.roadmap_file = output_dir / "roadmap.json"
self.discovery_file = output_dir / "roadmap_discovery.json"
self.project_index_file = output_dir / "project_index.json"
async def execute(self) -> RoadmapPhaseResult:
"""Generate and prioritize features for the roadmap."""
debug("roadmap_phase", "Starting phase: features")
if not self.discovery_file.exists():
debug_error(
"roadmap_phase",
"Discovery file not found - cannot generate features",
discovery_file=str(self.discovery_file),
)
return RoadmapPhaseResult(
"features", False, [], ["Discovery file not found"], 0
)
if self.roadmap_file.exists() and not self.refresh:
debug("roadmap_phase", "roadmap.json already exists, skipping")
print_status("roadmap.json already exists", "success")
return RoadmapPhaseResult("features", True, [str(self.roadmap_file)], [], 0)
errors = []
for attempt in range(MAX_RETRIES):
debug("roadmap_phase", f"Features attempt {attempt + 1}/{MAX_RETRIES}")
print_status(
f"Running feature generation agent (attempt {attempt + 1})...",
"progress",
)
context = self._build_context()
success, output = await self.agent_executor.run_agent(
"roadmap_features.md",
additional_context=context,
)
if success and self.roadmap_file.exists():
validation_result = self._validate_features(attempt)
if validation_result is not None:
return validation_result
errors.append(f"Validation failed on attempt {attempt + 1}")
else:
debug_warning(
"roadmap_phase",
f"Features attempt {attempt + 1} failed - file not created",
)
errors.append(
f"Attempt {attempt + 1}: Agent did not create roadmap file"
)
debug_error(
"roadmap_phase", "Features phase failed after all retries", errors=errors
)
return RoadmapPhaseResult("features", False, [], errors, MAX_RETRIES)
def _build_context(self) -> str:
"""Build context string for the features agent."""
return f"""
**Discovery File**: {self.discovery_file}
**Project Index**: {self.project_index_file}
**Output File**: {self.roadmap_file}
Based on the discovery data:
1. Generate features that address user pain points
2. Prioritize using MoSCoW framework
3. Organize into phases
4. Create milestones
5. Map dependencies
Output the complete roadmap to roadmap.json.
"""
def _validate_features(self, attempt: int) -> RoadmapPhaseResult | None:
"""Validate the roadmap features file.
Returns RoadmapPhaseResult if validation succeeds, None otherwise.
"""
try:
with open(self.roadmap_file) as f:
data = json.load(f)
required = ["phases", "features", "vision"]
missing = [k for k in required if k not in data]
feature_count = len(data.get("features", []))
debug_detailed(
"roadmap_phase",
"Validating roadmap.json",
missing_fields=missing,
feature_count=feature_count,
)
if not missing and feature_count >= 3:
debug_success(
"roadmap_phase",
"Created valid roadmap.json",
attempt=attempt + 1,
feature_count=feature_count,
)
print_status("Created valid roadmap.json", "success")
return RoadmapPhaseResult(
"features", True, [str(self.roadmap_file)], [], attempt
)
else:
if missing:
debug_warning(
"roadmap_phase", f"Missing required fields: {missing}"
)
else:
debug_warning(
"roadmap_phase",
f"Roadmap has only {feature_count} features (min 3)",
)
return None
except json.JSONDecodeError as e:
debug_error("roadmap_phase", "Invalid JSON in roadmap file", error=str(e))
return None