Files
Aperant/auto-claude/implementation_plan.py
T
2025-12-15 21:10:27 +01:00

898 lines
32 KiB
Python

#!/usr/bin/env python3
"""
Implementation Plan Manager
============================
Core data structures and utilities for subtask-based implementation plans.
Replaces the test-centric feature_list.json with implementation_plan.json.
The key insight: Tests verify outcomes, but SUBTASKS define implementation steps.
For complex multi-service features, implementation order matters.
Workflow Types:
- feature: Standard multi-service feature (phases = services)
- refactor: Migration/refactor work (phases = stages: add, migrate, remove)
- investigation: Bug hunting (phases = investigate, hypothesize, fix)
- migration: Data migration (phases = prepare, test, execute, cleanup)
- simple: Single-service enhancement (minimal overhead)
"""
import json
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
class WorkflowType(str, Enum):
"""Types of workflows with different phase structures."""
FEATURE = "feature" # Multi-service feature (phases = services)
REFACTOR = "refactor" # Stage-based (add new, migrate, remove old)
INVESTIGATION = "investigation" # Bug hunting (investigate, hypothesize, fix)
MIGRATION = "migration" # Data migration (prepare, test, execute, cleanup)
SIMPLE = "simple" # Single-service, minimal overhead
DEVELOPMENT = "development" # General development work
ENHANCEMENT = "enhancement" # Improving existing features
class PhaseType(str, Enum):
"""Types of phases within a workflow."""
SETUP = "setup" # Project scaffolding, environment setup
IMPLEMENTATION = "implementation" # Writing code
INVESTIGATION = "investigation" # Research, debugging, analysis
INTEGRATION = "integration" # Wiring services together
CLEANUP = "cleanup" # Removing old code, polish
class SubtaskStatus(str, Enum):
"""Status of a subtask."""
PENDING = "pending" # Not started
IN_PROGRESS = "in_progress" # Currently being worked on
COMPLETED = "completed" # Completed successfully (matches JSON format)
BLOCKED = "blocked" # Can't start (dependency not met or undefined)
FAILED = "failed" # Attempted but failed
class VerificationType(str, Enum):
"""How to verify a subtask is complete."""
COMMAND = "command" # Run a shell command
API = "api" # Make an API request
BROWSER = "browser" # Browser automation check
COMPONENT = "component" # Component renders correctly
MANUAL = "manual" # Requires human verification
NONE = "none" # No verification needed (investigation)
@dataclass
class Verification:
"""How to verify a subtask is complete."""
type: VerificationType
run: str | None = None # Command to run
url: str | None = None # URL for API/browser tests
method: str | None = None # HTTP method for API tests
expect_status: int | None = None # Expected HTTP status
expect_contains: str | None = None # Expected content
scenario: str | None = None # Description for browser/manual tests
def to_dict(self) -> dict:
result = {"type": self.type.value}
for key in [
"run",
"url",
"method",
"expect_status",
"expect_contains",
"scenario",
]:
val = getattr(self, key)
if val is not None:
result[key] = val
return result
@classmethod
def from_dict(cls, data: dict) -> "Verification":
return cls(
type=VerificationType(data.get("type", "none")),
run=data.get("run"),
url=data.get("url"),
method=data.get("method"),
expect_status=data.get("expect_status"),
expect_contains=data.get("expect_contains"),
scenario=data.get("scenario"),
)
@dataclass
class Subtask:
"""A single unit of implementation work."""
id: str
description: str
status: SubtaskStatus = SubtaskStatus.PENDING
# Scoping
service: str | None = None # Which service (backend, frontend, worker)
all_services: bool = False # True for integration subtasks
# Files
files_to_modify: list[str] = field(default_factory=list)
files_to_create: list[str] = field(default_factory=list)
patterns_from: list[str] = field(default_factory=list)
# Verification
verification: Verification | None = None
# For investigation subtasks
expected_output: str | None = None # Knowledge/decision output
actual_output: str | None = None # What was discovered
# Tracking
started_at: str | None = None
completed_at: str | None = None
session_id: int | None = None # Which session completed this
# Self-Critique
critique_result: dict | None = None # Results from self-critique before completion
def to_dict(self) -> dict:
result = {
"id": self.id,
"description": self.description,
"status": self.status.value,
}
if self.service:
result["service"] = self.service
if self.all_services:
result["all_services"] = True
if self.files_to_modify:
result["files_to_modify"] = self.files_to_modify
if self.files_to_create:
result["files_to_create"] = self.files_to_create
if self.patterns_from:
result["patterns_from"] = self.patterns_from
if self.verification:
result["verification"] = self.verification.to_dict()
if self.expected_output:
result["expected_output"] = self.expected_output
if self.actual_output:
result["actual_output"] = self.actual_output
if self.started_at:
result["started_at"] = self.started_at
if self.completed_at:
result["completed_at"] = self.completed_at
if self.session_id is not None:
result["session_id"] = self.session_id
if self.critique_result:
result["critique_result"] = self.critique_result
return result
@classmethod
def from_dict(cls, data: dict) -> "Subtask":
verification = None
if "verification" in data:
verification = Verification.from_dict(data["verification"])
return cls(
id=data["id"],
description=data["description"],
status=SubtaskStatus(data.get("status", "pending")),
service=data.get("service"),
all_services=data.get("all_services", False),
files_to_modify=data.get("files_to_modify", []),
files_to_create=data.get("files_to_create", []),
patterns_from=data.get("patterns_from", []),
verification=verification,
expected_output=data.get("expected_output"),
actual_output=data.get("actual_output"),
started_at=data.get("started_at"),
completed_at=data.get("completed_at"),
session_id=data.get("session_id"),
critique_result=data.get("critique_result"),
)
def start(self, session_id: int):
"""Mark subtask as in progress."""
self.status = SubtaskStatus.IN_PROGRESS
self.started_at = datetime.now().isoformat()
self.session_id = session_id
# Clear stale data from previous runs to ensure clean state
self.completed_at = None
self.actual_output = None
def complete(self, output: str | None = None):
"""Mark subtask as done."""
self.status = SubtaskStatus.COMPLETED
self.completed_at = datetime.now().isoformat()
if output:
self.actual_output = output
def fail(self, reason: str | None = None):
"""Mark subtask as failed."""
self.status = SubtaskStatus.FAILED
self.completed_at = None # Clear to maintain consistency (failed != completed)
if reason:
self.actual_output = f"FAILED: {reason}"
@dataclass
class Phase:
"""A group of subtasks with dependencies."""
phase: int
name: str
type: PhaseType = PhaseType.IMPLEMENTATION
subtasks: list[Subtask] = field(default_factory=list)
depends_on: list[int] = field(default_factory=list)
parallel_safe: bool = False # Can subtasks in this phase run in parallel?
# Backwards compatibility: chunks is an alias for subtasks
@property
def chunks(self) -> list[Subtask]:
"""Alias for subtasks (backwards compatibility)."""
return self.subtasks
@chunks.setter
def chunks(self, value: list[Subtask]):
"""Alias for subtasks (backwards compatibility)."""
self.subtasks = value
def to_dict(self) -> dict:
result = {
"phase": self.phase,
"name": self.name,
"type": self.type.value,
"subtasks": [s.to_dict() for s in self.subtasks],
# Also include 'chunks' for backwards compatibility
"chunks": [s.to_dict() for s in self.subtasks],
}
if self.depends_on:
result["depends_on"] = self.depends_on
if self.parallel_safe:
result["parallel_safe"] = True
return result
@classmethod
def from_dict(cls, data: dict, fallback_phase: int = 1) -> "Phase":
"""Create Phase from dict. Uses fallback_phase if 'phase' field is missing."""
# Support both 'subtasks' and 'chunks' keys for backwards compatibility
subtask_data = data.get("subtasks", data.get("chunks", []))
return cls(
phase=data.get("phase", fallback_phase),
name=data.get("name", f"Phase {fallback_phase}"),
type=PhaseType(data.get("type", "implementation")),
subtasks=[Subtask.from_dict(s) for s in subtask_data],
depends_on=data.get("depends_on", []),
parallel_safe=data.get("parallel_safe", False),
)
def is_complete(self) -> bool:
"""Check if all subtasks in this phase are done."""
return all(s.status == SubtaskStatus.COMPLETED for s in self.subtasks)
def get_pending_subtasks(self) -> list[Subtask]:
"""Get subtasks that can be worked on."""
return [s for s in self.subtasks if s.status == SubtaskStatus.PENDING]
# Backwards compatibility alias
def get_pending_chunks(self) -> list[Subtask]:
"""Alias for get_pending_subtasks (backwards compatibility)."""
return self.get_pending_subtasks()
def get_progress(self) -> tuple[int, int]:
"""Get (completed, total) subtask counts."""
done = sum(1 for s in self.subtasks if s.status == SubtaskStatus.COMPLETED)
return done, len(self.subtasks)
@dataclass
class ImplementationPlan:
"""Complete implementation plan for a feature/task."""
feature: str
workflow_type: WorkflowType = WorkflowType.FEATURE
services_involved: list[str] = field(default_factory=list)
phases: list[Phase] = field(default_factory=list)
final_acceptance: list[str] = field(default_factory=list)
# Metadata
created_at: str | None = None
updated_at: str | None = None
spec_file: str | None = None
# Task status (synced with UI)
# status: backlog, in_progress, ai_review, human_review, done
# planStatus: pending, in_progress, review, completed
status: str | None = None
planStatus: str | None = None
recoveryNote: str | None = None
qa_signoff: dict | None = None
def to_dict(self) -> dict:
result = {
"feature": self.feature,
"workflow_type": self.workflow_type.value,
"services_involved": self.services_involved,
"phases": [p.to_dict() for p in self.phases],
"final_acceptance": self.final_acceptance,
"created_at": self.created_at,
"updated_at": self.updated_at,
"spec_file": self.spec_file,
}
# Include status fields if set (synced with UI)
if self.status:
result["status"] = self.status
if self.planStatus:
result["planStatus"] = self.planStatus
if self.recoveryNote:
result["recoveryNote"] = self.recoveryNote
if self.qa_signoff:
result["qa_signoff"] = self.qa_signoff
return result
@classmethod
def from_dict(cls, data: dict) -> "ImplementationPlan":
# Parse workflow_type with fallback for unknown types
workflow_type_str = data.get("workflow_type", "feature")
try:
workflow_type = WorkflowType(workflow_type_str)
except ValueError:
# Unknown workflow type - default to FEATURE
print(
f"Warning: Unknown workflow_type '{workflow_type_str}', defaulting to 'feature'"
)
workflow_type = WorkflowType.FEATURE
return cls(
feature=data["feature"],
workflow_type=workflow_type,
services_involved=data.get("services_involved", []),
phases=[
Phase.from_dict(p, idx + 1)
for idx, p in enumerate(data.get("phases", []))
],
final_acceptance=data.get("final_acceptance", []),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
spec_file=data.get("spec_file"),
status=data.get("status"),
planStatus=data.get("planStatus"),
recoveryNote=data.get("recoveryNote"),
qa_signoff=data.get("qa_signoff"),
)
def save(self, path: Path):
"""Save plan to JSON file."""
self.updated_at = datetime.now().isoformat()
if not self.created_at:
self.created_at = self.updated_at
# Auto-update status based on subtask completion
self.update_status_from_subtasks()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(self.to_dict(), f, indent=2)
def update_status_from_subtasks(self):
"""Update overall status and planStatus based on subtask completion state.
This syncs the task status with the UI's expected values:
- status: backlog, in_progress, ai_review, human_review, done
- planStatus: pending, in_progress, review, completed
Note: Preserves human_review/review status when it represents plan approval stage
(all subtasks pending but user needs to approve the plan before coding starts).
"""
all_subtasks = [s for p in self.phases for s in p.subtasks]
if not all_subtasks:
# No subtasks yet - stay in backlog/pending
if not self.status:
self.status = "backlog"
if not self.planStatus:
self.planStatus = "pending"
return
completed_count = sum(
1 for s in all_subtasks if s.status == SubtaskStatus.COMPLETED
)
failed_count = sum(1 for s in all_subtasks if s.status == SubtaskStatus.FAILED)
in_progress_count = sum(
1 for s in all_subtasks if s.status == SubtaskStatus.IN_PROGRESS
)
total_count = len(all_subtasks)
# Determine status based on subtask states
if completed_count == total_count:
# All subtasks completed - check if QA approved
if self.qa_signoff and self.qa_signoff.get("status") == "approved":
self.status = "human_review"
self.planStatus = "review"
else:
# All subtasks done, waiting for QA
self.status = "ai_review"
self.planStatus = "review"
elif failed_count > 0:
# Some subtasks failed - still in progress (needs retry or fix)
self.status = "in_progress"
self.planStatus = "in_progress"
elif in_progress_count > 0 or completed_count > 0:
# Some subtasks in progress or completed
self.status = "in_progress"
self.planStatus = "in_progress"
else:
# All subtasks pending
# Preserve human_review/review status if it's for plan approval stage
# (spec is complete, waiting for user to approve before coding starts)
if self.status == "human_review" and self.planStatus == "review":
# Keep the plan approval status - don't reset to backlog
pass
else:
self.status = "backlog"
self.planStatus = "pending"
@classmethod
def load(cls, path: Path) -> "ImplementationPlan":
"""Load plan from JSON file."""
with open(path) as f:
return cls.from_dict(json.load(f))
def get_available_phases(self) -> list[Phase]:
"""Get phases whose dependencies are satisfied."""
completed_phases = {p.phase for p in self.phases if p.is_complete()}
available = []
for phase in self.phases:
if phase.is_complete():
continue
deps_met = all(d in completed_phases for d in phase.depends_on)
if deps_met:
available.append(phase)
return available
def get_next_subtask(self) -> tuple[Phase, Subtask] | None:
"""Get the next subtask to work on, respecting dependencies."""
for phase in self.get_available_phases():
pending = phase.get_pending_subtasks()
if pending:
return phase, pending[0]
return None
def get_progress(self) -> dict:
"""Get overall progress statistics."""
total_subtasks = sum(len(p.subtasks) for p in self.phases)
done_subtasks = sum(
1
for p in self.phases
for s in p.subtasks
if s.status == SubtaskStatus.COMPLETED
)
failed_subtasks = sum(
1
for p in self.phases
for s in p.subtasks
if s.status == SubtaskStatus.FAILED
)
completed_phases = sum(1 for p in self.phases if p.is_complete())
return {
"total_phases": len(self.phases),
"completed_phases": completed_phases,
"total_subtasks": total_subtasks,
"completed_subtasks": done_subtasks,
"failed_subtasks": failed_subtasks,
"percent_complete": round(100 * done_subtasks / total_subtasks, 1)
if total_subtasks > 0
else 0,
"is_complete": done_subtasks == total_subtasks and failed_subtasks == 0,
}
def get_status_summary(self) -> str:
"""Get a human-readable status summary."""
progress = self.get_progress()
lines = [
f"Feature: {self.feature}",
f"Workflow: {self.workflow_type.value}",
f"Progress: {progress['completed_subtasks']}/{progress['total_subtasks']} subtasks ({progress['percent_complete']}%)",
f"Phases: {progress['completed_phases']}/{progress['total_phases']} complete",
]
if progress["failed_subtasks"] > 0:
lines.append(
f"Failed: {progress['failed_subtasks']} subtasks need attention"
)
if progress["is_complete"]:
lines.append("Status: COMPLETE - Ready for final acceptance testing")
else:
next_work = self.get_next_subtask()
if next_work:
phase, subtask = next_work
lines.append(
f"Next: Phase {phase.phase} ({phase.name}) - {subtask.description}"
)
else:
lines.append("Status: BLOCKED - No available subtasks")
return "\n".join(lines)
def add_followup_phase(
self,
name: str,
subtasks: list[Subtask],
phase_type: PhaseType = PhaseType.IMPLEMENTATION,
parallel_safe: bool = False,
) -> Phase:
"""
Add a new follow-up phase to an existing (typically completed) plan.
This allows users to extend completed builds with additional work.
The new phase depends on all existing phases to ensure proper sequencing.
Args:
name: Name of the follow-up phase (e.g., "Follow-Up: Add validation")
subtasks: List of Subtask objects to include in the phase
phase_type: Type of the phase (default: implementation)
parallel_safe: Whether subtasks in this phase can run in parallel
Returns:
The newly created Phase object
Example:
>>> plan = ImplementationPlan.load(plan_path)
>>> new_subtasks = [Subtask(id="followup-1", description="Add error handling")]
>>> plan.add_followup_phase("Follow-Up: Error Handling", new_subtasks)
>>> plan.save(plan_path)
"""
# Calculate the next phase number
if self.phases:
next_phase_num = max(p.phase for p in self.phases) + 1
# New phase depends on all existing phases
depends_on = [p.phase for p in self.phases]
else:
next_phase_num = 1
depends_on = []
# Create the new phase
new_phase = Phase(
phase=next_phase_num,
name=name,
type=phase_type,
subtasks=subtasks,
depends_on=depends_on,
parallel_safe=parallel_safe,
)
# Append to phases list
self.phases.append(new_phase)
# Update status to in_progress since we now have pending work
self.status = "in_progress"
self.planStatus = "in_progress"
# Clear QA signoff since the plan has changed
self.qa_signoff = None
return new_phase
def reset_for_followup(self) -> bool:
"""
Reset plan status from completed/done back to in_progress for follow-up work.
This method is called when a user wants to add follow-up tasks to a
completed build. It transitions the plan status back to in_progress
so the build pipeline can continue processing new subtasks.
The method:
- Sets status to "in_progress" (from "done", "ai_review", "human_review")
- Sets planStatus to "in_progress" (from "completed", "review")
- Clears QA signoff since new work invalidates previous approval
- Clears recovery notes from previous run
Returns:
bool: True if reset was successful, False if plan wasn't in a
completed/reviewable state
Example:
>>> plan = ImplementationPlan.load(plan_path)
>>> if plan.reset_for_followup():
... plan.add_followup_phase("New Work", subtasks)
... plan.save(plan_path)
"""
# States that indicate the plan is "complete" or in review
completed_statuses = {"done", "ai_review", "human_review"}
completed_plan_statuses = {"completed", "review"}
# Check if plan is actually in a completed/reviewable state
is_completed = (
self.status in completed_statuses
or self.planStatus in completed_plan_statuses
)
# Also check if all subtasks are actually completed
all_subtasks = [s for p in self.phases for s in p.subtasks]
all_subtasks_done = all_subtasks and all(
s.status == SubtaskStatus.COMPLETED for s in all_subtasks
)
if not (is_completed or all_subtasks_done):
# Plan is not in a state that needs resetting
return False
# Transition back to in_progress
self.status = "in_progress"
self.planStatus = "in_progress"
# Clear QA signoff since we're adding new work
self.qa_signoff = None
# Clear any recovery notes from previous run
self.recoveryNote = None
return True
def create_feature_plan(
feature: str,
services: list[str],
phases_config: list[dict],
) -> ImplementationPlan:
"""
Create a standard feature implementation plan.
Args:
feature: Name of the feature
services: List of services involved
phases_config: List of phase configurations
Returns:
ImplementationPlan ready for use
"""
phases = []
for i, config in enumerate(phases_config, 1):
subtasks = [Subtask.from_dict(s) for s in config.get("subtasks", [])]
phase = Phase(
phase=i,
name=config["name"],
type=PhaseType(config.get("type", "implementation")),
subtasks=subtasks,
depends_on=config.get("depends_on", []),
parallel_safe=config.get("parallel_safe", False),
)
phases.append(phase)
return ImplementationPlan(
feature=feature,
workflow_type=WorkflowType.FEATURE,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
def create_investigation_plan(
bug_description: str,
services: list[str],
) -> ImplementationPlan:
"""
Create an investigation plan for debugging.
This creates a structured approach:
1. Reproduce & Instrument
2. Investigate
3. Fix (blocked until investigation complete)
"""
phases = [
Phase(
phase=1,
name="Reproduce & Instrument",
type=PhaseType.INVESTIGATION,
subtasks=[
Subtask(
id="add-logging",
description="Add detailed logging around suspected areas",
expected_output="Logs capture relevant state and events",
),
Subtask(
id="create-repro",
description="Create reliable reproduction steps",
expected_output="Can reproduce bug on demand",
),
],
),
Phase(
phase=2,
name="Identify Root Cause",
type=PhaseType.INVESTIGATION,
depends_on=[1],
subtasks=[
Subtask(
id="analyze",
description="Analyze logs and behavior",
expected_output="Root cause hypothesis with evidence",
),
],
),
Phase(
phase=3,
name="Implement Fix",
type=PhaseType.IMPLEMENTATION,
depends_on=[2],
subtasks=[
Subtask(
id="fix",
description="[TO BE DETERMINED FROM INVESTIGATION]",
status=SubtaskStatus.BLOCKED,
),
Subtask(
id="regression-test",
description="Add regression test to prevent recurrence",
status=SubtaskStatus.BLOCKED,
),
],
),
]
return ImplementationPlan(
feature=f"Fix: {bug_description}",
workflow_type=WorkflowType.INVESTIGATION,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
def create_refactor_plan(
refactor_description: str,
services: list[str],
stages: list[dict],
) -> ImplementationPlan:
"""
Create a refactor plan with stage-based phases.
Typical stages:
1. Add new system alongside old
2. Migrate consumers
3. Remove old system
4. Cleanup
"""
phases = []
for i, stage in enumerate(stages, 1):
subtasks = [Subtask.from_dict(s) for s in stage.get("subtasks", [])]
phase = Phase(
phase=i,
name=stage["name"],
type=PhaseType(stage.get("type", "implementation")),
subtasks=subtasks,
depends_on=stage.get("depends_on", [i - 1] if i > 1 else []),
)
phases.append(phase)
return ImplementationPlan(
feature=refactor_description,
workflow_type=WorkflowType.REFACTOR,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
# CLI for testing
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python implementation_plan.py <plan.json>")
print(" python implementation_plan.py --demo")
sys.exit(1)
if sys.argv[1] == "--demo":
# Create a demo plan
plan = create_feature_plan(
feature="Avatar Upload with Processing",
services=["backend", "worker", "frontend"],
phases_config=[
{
"name": "Backend Foundation",
"parallel_safe": True,
"subtasks": [
{
"id": "avatar-model",
"service": "backend",
"description": "Add avatar fields to User model",
"files_to_modify": ["app/models/user.py"],
"files_to_create": ["migrations/add_avatar.py"],
"verification": {
"type": "command",
"run": "flask db upgrade",
},
},
{
"id": "avatar-endpoint",
"service": "backend",
"description": "POST /api/users/avatar endpoint",
"files_to_modify": ["app/routes/users.py"],
"patterns_from": ["app/routes/profile.py"],
"verification": {
"type": "api",
"method": "POST",
"url": "/api/users/avatar",
},
},
],
},
{
"name": "Worker Pipeline",
"depends_on": [1],
"subtasks": [
{
"id": "image-task",
"service": "worker",
"description": "Celery task for image processing",
"files_to_create": ["app/tasks/images.py"],
"patterns_from": ["app/tasks/reports.py"],
},
],
},
{
"name": "Frontend",
"depends_on": [1],
"subtasks": [
{
"id": "avatar-component",
"service": "frontend",
"description": "AvatarUpload React component",
"files_to_create": ["src/components/AvatarUpload.tsx"],
"patterns_from": ["src/components/FileUpload.tsx"],
},
],
},
{
"name": "Integration",
"depends_on": [2, 3],
"type": "integration",
"subtasks": [
{
"id": "e2e-wiring",
"all_services": True,
"description": "Connect frontend → backend → worker",
"verification": {
"type": "browser",
"scenario": "Upload → Process → Display",
},
},
],
},
],
)
plan.final_acceptance = [
"User can upload avatar from profile page",
"Avatar is automatically resized",
"Large/invalid files show error",
]
print(json.dumps(plan.to_dict(), indent=2))
print("\n---\n")
print(plan.get_status_summary())
else:
# Load and display existing plan
plan = ImplementationPlan.load(Path(sys.argv[1]))
print(plan.get_status_summary())
# =============================================================================
# BACKWARDS COMPATIBILITY ALIASES
# =============================================================================
# These aliases maintain compatibility with code that uses the old "chunk"
# terminology. New code should use Subtask/SubtaskStatus.
Chunk = Subtask
ChunkStatus = SubtaskStatus