diff --git a/.amplifier/project.json b/.amplifier/project.json new file mode 100644 index 00000000..3bfdc9c6 --- /dev/null +++ b/.amplifier/project.json @@ -0,0 +1,7 @@ +{ + "project_id": "test_integration_project_20251007_121405", + "project_name": "Test Integration Project", + "has_planning": true, + "created_at": "2025-10-07T12:14:05.201566", + "updated_at": "2025-10-07T12:14:05.201569" +} \ No newline at end of file diff --git a/Makefile b/Makefile index 66c75cd1..7980343c 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,13 @@ default: ## Show essential commands @echo "AI Context:" @echo " make ai-context-files Build AI context documentation" @echo "" + @echo "Project Planning:" + @echo " make project-init Initialize project with AI planning" + @echo " make project-plan Generate and execute project plan" + @echo " make project-status Show project status and progress" + @echo " make project-execute Execute project tasks with orchestration" + @echo " make smart-decomposer Smart task decomposition CLI" + @echo "" @echo "Blog Writing:" @echo " make blog-write Create a blog post from your ideas" @echo "" @@ -180,7 +187,7 @@ check: ## Format, lint, and type-check all code @echo "Type-checking code with pyright..." @VIRTUAL_ENV= uv run pyright @echo "Checking for stubs and placeholders..." - @python tools/check_stubs.py + @uv run python tools/check_stubs.py @echo "All checks passed!" test: ## Run all tests @@ -484,6 +491,69 @@ ai-context-files: ## Build AI context files uv run python tools/build_git_collector_files.py @echo "AI context files generated" +# Project Planning +project-init: ## Initialize project with AI planning. Usage: make project-init PROJECT_NAME="My Project" [PROJECT_PATH=/path/to/project] + @if [ -z "$(PROJECT_NAME)" ]; then \ + echo "Error: Please provide a project name. Usage: make project-init PROJECT_NAME=\"My Project\""; \ + exit 1; \ + fi + @echo "πŸš€ Initializing AI-driven project planning..."; \ + echo " Project: $(PROJECT_NAME)"; \ + if [ -n "$(PROJECT_PATH)" ]; then \ + echo " Path: $(PROJECT_PATH)"; \ + uv run python -m scenarios.project_planner init --name "$(PROJECT_NAME)" --path "$(PROJECT_PATH)"; \ + else \ + uv run python -m scenarios.project_planner init --name "$(PROJECT_NAME)"; \ + fi + +project-plan: ## Generate and execute project plan. Usage: make project-plan PROJECT_ID="proj-abc123" [GOAL="Custom goal"] + @if [ -z "$(PROJECT_ID)" ]; then \ + echo "Error: Please provide a project ID. Usage: make project-plan PROJECT_ID=\"proj-abc123\""; \ + exit 1; \ + fi + @echo "πŸ“‹ Generating project plan..."; \ + if [ -n "$(GOAL)" ]; then \ + echo " Goal: $(GOAL)"; \ + uv run python -m scenarios.project_planner plan --project-id "$(PROJECT_ID)" --goal "$(GOAL)"; \ + else \ + uv run python -m scenarios.project_planner plan --project-id "$(PROJECT_ID)"; \ + fi + +project-status: ## Show project status and progress. Usage: make project-status PROJECT_ID="proj-abc123" + @if [ -z "$(PROJECT_ID)" ]; then \ + echo "Error: Please provide a project ID. Usage: make project-status PROJECT_ID=\"proj-abc123\""; \ + exit 1; \ + fi + @echo "πŸ“Š Checking project status..."; \ + uv run python -m scenarios.project_planner status --project-id "$(PROJECT_ID)" + +project-execute: ## Execute project tasks with orchestration. Usage: make project-execute PROJECT_ID="proj-abc123" [MAX_PARALLEL=5] + @if [ -z "$(PROJECT_ID)" ]; then \ + echo "Error: Please provide a project ID. Usage: make project-execute PROJECT_ID=\"proj-abc123\""; \ + exit 1; \ + fi + @echo "πŸš€ Executing project tasks..."; \ + if [ -n "$(MAX_PARALLEL)" ]; then \ + uv run python -m scenarios.project_planner execute --project-id "$(PROJECT_ID)" --max-parallel "$(MAX_PARALLEL)"; \ + else \ + uv run python -m scenarios.project_planner execute --project-id "$(PROJECT_ID)"; \ + fi + +smart-decomposer: ## Smart task decomposition CLI. Usage: make smart-decomposer COMMAND="decompose --goal 'Build feature X'" + @if [ -z "$(COMMAND)" ]; then \ + echo "Usage: make smart-decomposer COMMAND=\"decompose --goal 'Build feature X'\""; \ + echo ""; \ + echo "Available commands:"; \ + echo " decompose --goal 'Goal text' Decompose goal into tasks"; \ + echo " assign --project-id ID Assign agents to tasks"; \ + echo " execute --project-id ID Execute tasks with orchestration"; \ + echo " status --project-id ID Show project status"; \ + echo ""; \ + exit 1; \ + fi + @echo "🧠 Running smart decomposer..."; \ + uv run python -m scenarios.smart_decomposer $(COMMAND) + # Blog Writing blog-write: ## Create a blog post from your ideas. Usage: make blog-write IDEA=ideas.md WRITINGS=my_writings/ [INSTRUCTIONS="..."] @if [ -z "$(IDEA)" ]; then \ diff --git a/amplifier/core/__init__.py b/amplifier/core/__init__.py new file mode 100644 index 00000000..2a4b41b9 --- /dev/null +++ b/amplifier/core/__init__.py @@ -0,0 +1,11 @@ +""" +Amplifier Core - Project context and session management. + +Provides automatic project detection and persistent state management +for amplifier across multiple invocations. +""" + +from .context import ProjectContext +from .context import detect_project_context + +__all__ = ["ProjectContext", "detect_project_context"] diff --git a/amplifier/core/context.py b/amplifier/core/context.py new file mode 100644 index 00000000..83fe8340 --- /dev/null +++ b/amplifier/core/context.py @@ -0,0 +1,121 @@ +""" +Project Context Detection and Management. + +Automatically detects project planning context and provides persistent state +management across amplifier invocations. +""" + +import json +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path + +from amplifier.planner import Project +from amplifier.planner import load_project + + +@dataclass +class ProjectContext: + """Amplifier project context with planning integration.""" + + project_root: Path + project_id: str + project_name: str + config_file: Path + planner_project: Project | None = None + has_planning: bool = False + + @classmethod + def from_config(cls, config_path: Path) -> "ProjectContext": + """Create context from .amplifier/project.json config.""" + with open(config_path) as f: + config = json.load(f) + + project_root = config_path.parent.parent + context = cls( + project_root=project_root, + project_id=config["project_id"], + project_name=config["project_name"], + config_file=config_path, + has_planning=config.get("has_planning", False), + ) + + # Load planner project if planning is enabled + if context.has_planning: + import contextlib + + with contextlib.suppress(FileNotFoundError): + context.planner_project = load_project(context.project_id) + + return context + + def enable_planning(self) -> None: + """Enable planning for this project.""" + if not self.has_planning: + self.has_planning = True + self.save_config() + + def save_config(self) -> None: + """Save project configuration.""" + config = { + "project_id": self.project_id, + "project_name": self.project_name, + "has_planning": self.has_planning, + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + } + + self.config_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.config_file, "w") as f: + json.dump(config, f, indent=2) + + +def detect_project_context(start_dir: Path | None = None) -> ProjectContext | None: + """ + Detect amplifier project context by looking for .amplifier/project.json. + + Walks up directory tree from start_dir (or cwd) looking for project config. + Returns None if no project context found. + """ + if start_dir is None: + start_dir = Path.cwd() + + # Walk up directory tree looking for .amplifier/project.json + current = Path(start_dir).absolute() + + while current != current.parent: # Not at filesystem root + config_file = current / ".amplifier" / "project.json" + if config_file.exists(): + try: + return ProjectContext.from_config(config_file) + except (json.JSONDecodeError, KeyError, OSError): + # Invalid config, continue searching up + pass + current = current.parent + + return None + + +def create_project_context( + project_name: str, project_root: Path | None = None, enable_planning: bool = True +) -> ProjectContext: + """Create new amplifier project context.""" + if project_root is None: + project_root = Path.cwd() + + # Generate project ID from name and timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + project_id = f"{project_name.lower().replace(' ', '_')}_{timestamp}" + + config_file = project_root / ".amplifier" / "project.json" + + context = ProjectContext( + project_root=project_root, + project_id=project_id, + project_name=project_name, + config_file=config_file, + has_planning=enable_planning, + ) + + context.save_config() + return context diff --git a/amplifier/planner/PHASE_2_PLAN.md b/amplifier/planner/PHASE_2_PLAN.md new file mode 100644 index 00000000..f25a927a --- /dev/null +++ b/amplifier/planner/PHASE_2_PLAN.md @@ -0,0 +1,189 @@ +# Super-Planner Phase 2 Implementation Plan + +## Status: Ready to Begin + +**Phase 1 Foundation**: βœ… COMPLETED +- Core models and storage (139 lines) +- 16 comprehensive tests passing +- Clean public API following amplifier philosophy + +## Phase 2 Architecture: Hybrid CLI Tools + +Based on amplifier-cli-architect guidance, Phase 2 will be implemented as CLI tools in `scenarios/planner/` using the hybrid "code for structure, AI for intelligence" pattern. + +### Target Structure + +``` +amplifier/planner/ # Phase 1 foundation (UNCHANGED) +β”œβ”€β”€ models.py # Core data models +β”œβ”€β”€ storage.py # Persistence layer +└── tests/ + +scenarios/planner/ # Phase 2 CLI tools (NEW) +β”œβ”€β”€ __init__.py +β”œβ”€β”€ planner_cli.py # Main CLI entry point +β”œβ”€β”€ decomposer.py # Task decomposition with AI +β”œβ”€β”€ strategic_assigner.py # Agent assignment logic +β”œβ”€β”€ session_wrapper.py # CCSDK SessionManager integration +β”œβ”€β”€ README.md # Modeled after @scenarios/blog_writer/ +β”œβ”€β”€ HOW_TO_CREATE_YOUR_OWN.md +β”œβ”€β”€ tests/ +└── examples/ +``` + +### Implementation Strategy + +#### 1. Use CCSDK Toolkit Foundation +- Start with `amplifier/ccsdk_toolkit/templates/tool_template.py` +- Import: `ClaudeSession`, `SessionManager`, `parse_llm_json` +- Use defensive LLM response handling from toolkit + +#### 2. Session Persistence Pattern +```python +async def decompose_task(task: Task, storage: Storage): + session_mgr = SessionManager() + session = session_mgr.load_or_create(f"planner_{task.id}") + + # Resume from existing decomposition + subtasks = session.context.get("subtasks", []) + + async with ClaudeSession(SessionOptions( + system_prompt="You are a task decomposition expert..." + )) as claude: + if not subtasks: + response = await claude.query(f"Decompose: {task.description}") + subtasks = parse_llm_json(response) + session.context["subtasks"] = subtasks + session_mgr.save(session) + + return subtasks +``` + +#### 3. Agent Spawning Integration +```python +from amplifier.tools import Task as AgentTask + +async def spawn_agents(assignments: list): + """Spawn multiple agents in parallel for assigned tasks""" + agent_calls = [] + for assignment in assignments: + agent_calls.append( + AgentTask.run( + agent=assignment['agent'], + prompt=assignment['prompt'] + ) + ) + results = await asyncio.gather(*agent_calls) + return results +``` + +### Make Command Integration + +Add to main Makefile: +```makefile +planner-create: ## Create new project with AI decomposition + @echo "Creating project with AI planning..." + uv run python -m scenarios.planner create $(ARGS) + +planner-plan: ## Decompose project tasks recursively + @echo "Planning project tasks..." + uv run python -m scenarios.planner plan --project-id=$(PROJECT_ID) + +planner-assign: ## Assign tasks to agents strategically + @echo "Assigning tasks to agents..." + uv run python -m scenarios.planner assign --project-id=$(PROJECT_ID) + +planner-execute: ## Execute assigned tasks via Task tool + @echo "Executing planned tasks..." + uv run python -m scenarios.planner execute --project-id=$(PROJECT_ID) +``` + +### Core Components + +#### 1. Task Decomposition (decomposer.py) +- **Purpose**: Use AI to break down high-level tasks into specific subtasks +- **Pattern**: Iterative decomposition with depth limits +- **Integration**: Uses CCSDK SessionManager for resume capability +- **Output**: Creates hierarchical Task objects using Phase 1 models + +#### 2. Strategic Assignment (strategic_assigner.py) +- **Purpose**: Analyze task requirements and assign to appropriate agents +- **Pattern**: Complexity analysis + capability matching +- **Integration**: Uses Task tool for agent spawning +- **Output**: Assignment queue with agent specifications + +#### 3. Session Management (session_wrapper.py) +- **Purpose**: Wrap CCSDK toolkit for planner-specific workflows +- **Pattern**: Project-level session persistence +- **Integration**: Bridges Phase 1 storage with CCSDK sessions +- **Output**: Resumable planning workflows + +### Key Implementation Principles + +#### 1. Hybrid Architecture +- **Code handles**: Project structure, task coordination, agent spawning +- **AI handles**: Task understanding, decomposition strategy, assignment logic + +#### 2. Incremental Progress +- Save after every major operation +- Support workflow interruption and resume +- Use SessionManager for session state + +#### 3. Defensive Programming +- Use `parse_llm_json` for all LLM responses +- Implement retry logic for API failures +- Handle cloud sync issues via existing file_io utilities + +#### 4. Integration with Phase 1 +- Import Phase 1 models: `from amplifier.planner import Task, Project, save_project, load_project` +- Extend but don't modify existing data models +- Use existing storage layer for persistence + +### Development Phases + +#### Phase 2a: Basic Decomposition (Week 1) +- CLI interface for project creation +- Simple task decomposition with AI +- Integration with Phase 1 storage + +#### Phase 2b: Strategic Assignment (Week 2) +- Task complexity analysis +- Agent capability matching +- Assignment queue generation + +#### Phase 2c: Execution Orchestration (Week 3) +- Agent spawning via Task tool +- Progress monitoring +- Status updates to project storage + +#### Phase 2d: Full Integration (Week 4) +- End-to-end workflows +- Comprehensive testing +- Documentation completion + +### Success Criteria + +- Can create projects from natural language descriptions +- Recursively decomposes tasks to actionable granularity +- Strategically assigns tasks based on complexity and agent capabilities +- Spawns multiple agents in parallel via Task tool +- Handles interruption and resume gracefully +- Follows amplifier's simplicity philosophy + +### Resources to Reference + +- **@scenarios/README.md**: Philosophy for user-facing tools +- **@scenarios/blog_writer/**: THE exemplar for documentation and structure +- **@amplifier/ccsdk_toolkit/DEVELOPER_GUIDE.md**: Complete technical guide +- **@amplifier/ccsdk_toolkit/templates/tool_template.py**: Starting point +- **@DISCOVERIES.md**: File I/O retry patterns and LLM response handling + +### Next Steps + +1. **Create scenarios/planner/ directory structure** +2. **Copy and customize tool_template.py as planner_cli.py** +3. **Implement basic decomposer.py using CCSDK patterns** +4. **Add make commands to main Makefile** +5. **Create README.md modeled after blog_writer/** + +This plan leverages amplifier's hybrid architecture while building on the solid Phase 1 foundation. The result will be a production-ready multi-agent planning system that embodies amplifier's design philosophy. \ No newline at end of file diff --git a/amplifier/planner/README.md b/amplifier/planner/README.md new file mode 100644 index 00000000..a1b18aea --- /dev/null +++ b/amplifier/planner/README.md @@ -0,0 +1,367 @@ +# Super-Planner: Multi-Agent Project Coordination + +Super-Planner is a core amplifier module that manages large projects with hierarchical tasks, supporting coordination between multiple AI agents and humans. It follows amplifier's "bricks and studs" philosophy with ruthless simplicity. + +## Quick Start + +```python +from amplifier.planner import Task, Project, TaskState, save_project, load_project +import uuid + +# Create a project +project = Project( + id=str(uuid.uuid4()), + name="Build Web App" +) + +# Add hierarchical tasks +backend = Task( + id="backend", + title="Create Backend API", + description="Build REST API with authentication" +) + +frontend = Task( + id="frontend", + title="Create Frontend", + description="React app with user interface", + depends_on=["backend"] # Depends on backend completion +) + +deployment = Task( + id="deployment", + title="Deploy Application", + depends_on=["backend", "frontend"] # Depends on both +) + +# Add tasks to project +project.add_task(backend) +project.add_task(frontend) +project.add_task(deployment) + +# Save to persistent storage +save_project(project) + +# Load later +loaded_project = load_project(project.id) +``` + +## Core Concepts + +### Task States + +Tasks progress through defined states: + +```python +from amplifier.planner import TaskState + +# Available states +TaskState.PENDING # Not started +TaskState.IN_PROGRESS # Currently being worked on +TaskState.COMPLETED # Finished successfully +TaskState.BLOCKED # Cannot proceed due to issues +``` + +### Task Dependencies + +Tasks can depend on other tasks being completed: + +```python +# Check if task can start based on completed dependencies +completed_tasks = {"backend", "database"} +if task.can_start(completed_tasks): + # All dependencies are met + task.state = TaskState.IN_PROGRESS +``` + +### Project Hierarchy + +Projects organize tasks in hierarchical structures: + +```python +# Get root tasks (no parents) +root_tasks = project.get_roots() + +# Get direct children of a task +sub_tasks = project.get_children("backend") + +# Navigate the task tree +for root in project.get_roots(): + print(f"Root: {root.title}") + for child in project.get_children(root.id): + print(f" - {child.title}") +``` + +## Usage Examples + +### Example 1: Simple Blog Project + +```python +import uuid +from amplifier.planner import Task, Project, TaskState, save_project + +# Create project +blog_project = Project( + id=str(uuid.uuid4()), + name="Django Blog" +) + +# Define tasks with hierarchy +tasks = [ + Task("setup", "Project Setup", "Initialize Django project"), + Task("models", "Create Models", "User, Post, Comment models", parent_id="setup"), + Task("views", "Create Views", "List, detail, create views", depends_on=["models"]), + Task("templates", "Create Templates", "HTML templates", depends_on=["views"]), + Task("tests", "Write Tests", "Unit and integration tests", depends_on=["models", "views"]), + Task("deploy", "Deploy", "Deploy to production", depends_on=["templates", "tests"]) +] + +# Add all tasks +for task in tasks: + blog_project.add_task(task) + +# Save project +save_project(blog_project) + +# Find tasks ready to start +completed_ids = {"setup"} # Setup is done +ready_tasks = [] +for task in blog_project.tasks.values(): + if task.state == TaskState.PENDING and task.can_start(completed_ids): + ready_tasks.append(task) + +print(f"Ready to start: {[t.title for t in ready_tasks]}") +# Output: Ready to start: ['Create Models'] +``` + +### Example 2: Complex E-commerce Platform + +```python +import uuid +from amplifier.planner import Task, Project, save_project + +# Create complex project +ecommerce = Project( + id=str(uuid.uuid4()), + name="E-commerce Platform" +) + +# Multi-level hierarchy +tasks = [ + # Backend foundation + Task("architecture", "Design Architecture", "System design and APIs"), + Task("auth", "Authentication Service", "User management", depends_on=["architecture"]), + Task("products", "Product Service", "Catalog management", depends_on=["architecture"]), + Task("orders", "Order Service", "Order processing", depends_on=["products", "auth"]), + + # Frontend components + Task("ui-kit", "UI Component Library", "Reusable components"), + Task("product-pages", "Product Pages", "Browse and detail pages", depends_on=["ui-kit", "products"]), + Task("checkout", "Checkout Flow", "Shopping cart and payment", depends_on=["product-pages", "orders"]), + + # Integration + Task("payment", "Payment Integration", "Stripe integration", depends_on=["orders"]), + Task("testing", "End-to-End Testing", "Full system tests", depends_on=["checkout", "payment"]), + Task("deployment", "Production Deployment", "Deploy all services", depends_on=["testing"]) +] + +for task in tasks: + ecommerce.add_task(task) + +save_project(ecommerce) + +# Analyze project structure +roots = ecommerce.get_roots() +print(f"Starting points: {[r.title for r in roots]}") + +# Find bottleneck tasks (many dependencies) +for task in ecommerce.tasks.values(): + if len(task.depends_on) > 1: + print(f"Complex task: {task.title} depends on {task.depends_on}") +``` + +### Example 3: Task State Management + +```python +from amplifier.planner import load_project, save_project, TaskState + +# Load existing project +project = load_project("some-project-id") + +# Update task states +project.tasks["backend"].state = TaskState.IN_PROGRESS +project.tasks["backend"].assigned_to = "agent-001" + +# Mark task as completed +project.tasks["backend"].state = TaskState.COMPLETED + +# Find next ready tasks +completed_ids = {task_id for task_id, task in project.tasks.items() + if task.state == TaskState.COMPLETED} + +next_tasks = [] +for task in project.tasks.values(): + if (task.state == TaskState.PENDING and + task.can_start(completed_ids)): + next_tasks.append(task) + +print(f"Next available: {[t.title for t in next_tasks]}") + +# Save updated state +save_project(project) +``` + +## Architecture + +### Design Philosophy + +Super-Planner follows amplifier's core principles: + +- **Ruthless Simplicity**: 139 lines total, no unnecessary abstractions +- **File-Based Storage**: JSON files in `data/planner/projects/` +- **Defensive I/O**: Uses amplifier's retry utilities for cloud sync resilience +- **Modular Design**: Clear contracts, regenerable internals + +### Module Structure + +``` +amplifier/planner/ +β”œβ”€β”€ __init__.py # Public API: Task, Project, TaskState, save_project, load_project +β”œβ”€β”€ models.py # Core data models (60 lines) +β”œβ”€β”€ storage.py # JSON persistence (70 lines) +└── tests/ + └── test_planner.py # Comprehensive test suite (16 tests) +``` + +### Data Model + +```python +@dataclass +class Task: + id: str # Unique identifier + title: str # Human-readable name + description: str # Detailed description + state: TaskState # Current state (enum) + parent_id: Optional[str] # Hierarchical parent + depends_on: List[str] # Task dependencies + assigned_to: Optional[str] # Agent or human assigned + created_at: str # ISO timestamp + updated_at: str # ISO timestamp + +@dataclass +class Project: + id: str # Unique identifier + name: str # Human-readable name + tasks: Dict[str, Task] # Task collection + created_at: str # ISO timestamp + updated_at: str # ISO timestamp +``` + +### Storage Format + +Projects are stored as JSON files in `data/planner/projects/{project_id}.json`: + +```json +{ + "id": "project-uuid", + "name": "My Project", + "created_at": "2024-01-15T10:30:00", + "updated_at": "2024-01-15T11:45:00", + "tasks": [ + { + "id": "task-1", + "title": "Setup Project", + "description": "Initialize repository and dependencies", + "state": "completed", + "parent_id": null, + "depends_on": [], + "assigned_to": "agent-001", + "created_at": "2024-01-15T10:30:00", + "updated_at": "2024-01-15T11:00:00" + } + ] +} +``` + +## Integration Points + +### Amplifier Integration + +Super-Planner is designed to integrate with amplifier's ecosystem: + +- **Task Tool**: Phase 2 will spawn sub-agents using `Task(subagent_type="...", prompt="...")` +- **CCSDK Toolkit**: Uses `SessionManager`, `parse_llm_json`, `retry_with_feedback` +- **File I/O**: Uses `amplifier.utils.file_io` for defensive operations +- **Data Directory**: Follows amplifier's `data/` convention + +### Defensive Programming + +Built-in resilience for common issues: + +```python +# Handles cloud sync delays (OneDrive, Dropbox, etc.) +from amplifier.utils.file_io import write_json, read_json + +# Automatic retries with exponential backoff +save_project(project) # Internally uses defensive file operations +``` + +## Phase 2 Roadmap + +The current implementation (Phase 1) provides a solid foundation. Phase 2 will add: + +### Intelligence Features (Phase 2) +- **LLM Integration**: Task decomposition using CCSDK SessionManager +- **Planning Mode**: Recursive task breakdown with AI assistance +- **Working Mode**: Strategic task assignment and agent spawning + +### Multi-Agent Coordination (Phase 3) +- **Agent Spawning**: Integration with amplifier's Task tool +- **Load Balancing**: Distribute work across multiple agents +- **Progress Monitoring**: Real-time coordination and status updates + +### Advanced Features (Phase 4+) +- **Git Integration**: Version control and distributed coordination +- **Performance Optimization**: Caching and selective loading +- **CLI Interface**: Make commands for project management + +## Testing + +Run the comprehensive test suite: + +```bash +# Run all planner tests +uv run pytest amplifier/planner/tests/ -v + +# Run specific test +uv run pytest amplifier/planner/tests/test_planner.py::TestTask::test_can_start_with_dependencies -v +``` + +Test coverage includes: +- Task creation and validation +- State management and transitions +- Dependency checking logic +- Project operations (hierarchy, children) +- Storage persistence (save/load round-trip) +- Integration workflows + +## Contributing + +When extending Super-Planner: + +1. **Follow amplifier's philosophy**: Ruthless simplicity, no unnecessary complexity +2. **Maintain the contract**: Public API in `__init__.py` should remain stable +3. **Test everything**: All functionality must have comprehensive tests +4. **Use existing patterns**: Leverage amplifier's utilities and conventions + +## Performance Considerations + +Current implementation targets: +- **Task Operations**: <1ms for state transitions and queries +- **Storage Operations**: <100ms for save/load (with defensive retries) +- **Memory Usage**: Minimal - load only current project data +- **Scalability**: Supports 1000+ tasks per project efficiently + +## License + +Part of the amplifier project. See main project license. \ No newline at end of file diff --git a/amplifier/planner/__init__.py b/amplifier/planner/__init__.py new file mode 100644 index 00000000..243d8754 --- /dev/null +++ b/amplifier/planner/__init__.py @@ -0,0 +1,33 @@ +"""Super-Planner: Multi-agent project coordination system.""" + +from amplifier.planner.agent_mapper import assign_agent +from amplifier.planner.agent_mapper import get_agent_workload +from amplifier.planner.agent_mapper import suggest_agent_for_domain +from amplifier.planner.decomposer import ProjectContext +from amplifier.planner.decomposer import decompose_goal +from amplifier.planner.decomposer import decompose_recursively +from amplifier.planner.models import Project +from amplifier.planner.models import Task +from amplifier.planner.models import TaskState +from amplifier.planner.orchestrator import ExecutionResults +from amplifier.planner.orchestrator import TaskResult +from amplifier.planner.orchestrator import orchestrate_execution +from amplifier.planner.storage import load_project +from amplifier.planner.storage import save_project + +__all__ = [ + "Task", + "Project", + "TaskState", + "save_project", + "load_project", + "orchestrate_execution", + "ExecutionResults", + "TaskResult", + "decompose_goal", + "decompose_recursively", + "ProjectContext", + "assign_agent", + "get_agent_workload", + "suggest_agent_for_domain", +] diff --git a/amplifier/planner/agent_mapper.py b/amplifier/planner/agent_mapper.py new file mode 100644 index 00000000..d5cc3ede --- /dev/null +++ b/amplifier/planner/agent_mapper.py @@ -0,0 +1,296 @@ +"""Agent Mapper Module for Super-Planner Phase 2 + +Purpose: Intelligent agent assignment based on task analysis and agent capabilities. +Contract: Maps tasks to the best-suited specialized agent from available pool. + +This module is a self-contained brick that delivers intelligent agent assignment +by analyzing task content and matching it against agent capabilities. + +Public Interface: + assign_agent(task: Task, available_agents: list[str]) -> str + Maps a task to the most appropriate agent based on task analysis. + Returns the agent name that best matches the task requirements. + +Dependencies: + - amplifier.planner.models.Task: Task data structure + - Standard library only (no external dependencies) +""" + +from __future__ import annotations + +import logging +import re + +from amplifier.planner.models import Task + +logger = logging.getLogger(__name__) + + +AGENT_CAPABILITIES = { + "zen-code-architect": { + "keywords": ["architecture", "design", "structure", "system", "blueprint", "specification"], + "patterns": ["design.*pattern", "architect.*", "system.*design", "module.*structure"], + "domains": ["architecture", "design", "planning"], + "priority": 9, + }, + "modular-builder": { + "keywords": ["module", "component", "build", "implement", "create", "construct", "develop"], + "patterns": ["build.*module", "create.*component", "implement.*feature", "develop.*"], + "domains": ["implementation", "coding", "building"], + "priority": 8, + }, + "bug-hunter": { + "keywords": ["bug", "error", "fix", "debug", "issue", "problem", "crash", "failure"], + "patterns": ["fix.*bug", "debug.*", "error.*handling", "solve.*issue", "troubleshoot"], + "domains": ["debugging", "fixing", "troubleshooting"], + "priority": 10, + }, + "test-coverage": { + "keywords": ["test", "testing", "coverage", "unit", "integration", "validation", "verify"], + "patterns": ["write.*test", "test.*coverage", "unit.*test", "integration.*test"], + "domains": ["testing", "validation", "quality"], + "priority": 7, + }, + "integration-specialist": { + "keywords": ["integrate", "api", "service", "connect", "interface", "endpoint", "webhook"], + "patterns": ["integrate.*service", "api.*connection", "connect.*system", "interface.*"], + "domains": ["integration", "apis", "connectivity"], + "priority": 8, + }, + "refactor-architect": { + "keywords": ["refactor", "restructure", "optimize", "improve", "clean", "simplify"], + "patterns": ["refactor.*code", "restructure.*", "optimize.*performance", "clean.*up"], + "domains": ["refactoring", "optimization", "cleanup"], + "priority": 7, + }, + "performance-optimizer": { + "keywords": ["performance", "speed", "optimize", "efficiency", "bottleneck", "slow"], + "patterns": ["improve.*performance", "optimize.*speed", "performance.*issue"], + "domains": ["performance", "optimization", "efficiency"], + "priority": 8, + }, + "database-architect": { + "keywords": ["database", "sql", "query", "schema", "migration", "table", "index"], + "patterns": ["database.*design", "sql.*query", "schema.*migration", "optimize.*query"], + "domains": ["database", "sql", "data"], + "priority": 7, + }, + "analysis-engine": { + "keywords": ["analyze", "analysis", "examine", "investigate", "study", "research"], + "patterns": ["analyze.*code", "investigate.*", "study.*pattern", "examine.*"], + "domains": ["analysis", "research", "investigation"], + "priority": 6, + }, + "content-researcher": { + "keywords": ["research", "documentation", "content", "write", "document", "readme"], + "patterns": ["write.*documentation", "research.*", "document.*", "create.*readme"], + "domains": ["documentation", "writing", "research"], + "priority": 5, + }, + "api-contract-designer": { + "keywords": ["api", "contract", "interface", "endpoint", "specification", "openapi"], + "patterns": ["design.*api", "api.*contract", "interface.*specification"], + "domains": ["api", "contracts", "specifications"], + "priority": 7, + }, + "ambiguity-guardian": { + "keywords": ["clarify", "ambiguous", "unclear", "vague", "requirements", "specification"], + "patterns": ["clarify.*requirements", "unclear.*specification", "ambiguous.*"], + "domains": ["requirements", "clarification", "validation"], + "priority": 6, + }, +} + +DEFAULT_AGENT = "modular-builder" + + +def _calculate_match_score(task: Task, agent_config: dict) -> float: + """Calculate how well an agent matches a task. + + Args: + task: The task to analyze + agent_config: Agent capability configuration + + Returns: + Score from 0.0 to 1.0 indicating match strength + """ + score = 0.0 + max_score = 0.0 + + task_text = f"{task.title} {task.description}".lower() + + # Keyword matching (40% weight) + keyword_matches = sum(1 for kw in agent_config["keywords"] if kw in task_text) + if agent_config["keywords"]: + score += (keyword_matches / len(agent_config["keywords"])) * 0.4 + max_score += 0.4 + + # Pattern matching (30% weight) + pattern_matches = 0 + for pattern in agent_config.get("patterns", []): + if re.search(pattern, task_text, re.IGNORECASE): + pattern_matches += 1 + if agent_config.get("patterns"): + score += (pattern_matches / len(agent_config["patterns"])) * 0.3 + max_score += 0.3 + + # Priority boost (30% weight) + priority = agent_config.get("priority", 5) / 10.0 + score += priority * 0.3 + max_score += 0.3 + + return score / max_score if max_score > 0 else 0.0 + + +def _filter_available_agents(available_agents: list[str]) -> dict: + """Filter capability map to only include available agents. + + Args: + available_agents: List of agent names that are available + + Returns: + Filtered capabilities dictionary + """ + filtered = {} + for agent_name in available_agents: + # Try exact match first + if agent_name in AGENT_CAPABILITIES: + filtered[agent_name] = AGENT_CAPABILITIES[agent_name] + else: + # Try normalized match (handle case differences) + normalized_name = agent_name.lower().replace("_", "-") + for known_agent, config in AGENT_CAPABILITIES.items(): + if known_agent.lower() == normalized_name: + filtered[agent_name] = config + break + + return filtered + + +def assign_agent(task: Task, available_agents: list[str]) -> str: + """Assign the most appropriate agent to a task. + + This function analyzes the task content and matches it against known + agent capabilities to find the best fit. It considers keywords, patterns, + and agent priority to make intelligent assignments. + + Args: + task: Task object containing title, description, and metadata + available_agents: List of available agent names to choose from + + Returns: + Name of the assigned agent (best match or default) + + Raises: + ValueError: If no agents are available + + Example: + >>> task = Task(id="1", title="Fix login bug", description="Users can't login") + >>> agents = ["bug-hunter", "modular-builder", "test-coverage"] + >>> assign_agent(task, agents) + "bug-hunter" + """ + if not available_agents: + raise ValueError("No agents available for assignment") + + # Get capabilities for available agents only + agent_capabilities = _filter_available_agents(available_agents) + + # If no known agents match, use first available as fallback + if not agent_capabilities: + logger.warning( + f"No known capabilities for agents: {available_agents}. Using first available: {available_agents[0]}" + ) + return available_agents[0] + + # Calculate scores for each agent + scores = {} + for agent_name, config in agent_capabilities.items(): + scores[agent_name] = _calculate_match_score(task, config) + + # Find best match + best_agent = max(scores, key=lambda k: scores[k]) + best_score = scores[best_agent] + + # Log assignment decision + if best_score > 0.3: + logger.info(f"Assigned task '{task.title}' to agent '{best_agent}' (score: {best_score:.2f})") + else: + # Low confidence match - use default if available + if DEFAULT_AGENT in available_agents: + logger.info(f"Low confidence match for task '{task.title}'. Using default agent: {DEFAULT_AGENT}") + return DEFAULT_AGENT + logger.warning( + f"Low confidence match for task '{task.title}'. " + f"Using best available: {best_agent} (score: {best_score:.2f})" + ) + + return best_agent + + +def get_agent_workload(tasks: list[Task]) -> dict: + """Calculate workload distribution across agents. + + Analyzes a list of tasks to determine how many tasks are assigned + to each agent. Useful for load balancing and capacity planning. + + Args: + tasks: List of tasks with agent assignments + + Returns: + Dictionary mapping agent names to task counts + + Example: + >>> tasks = [ + ... Task(id="1", title="Bug", assigned_to="bug-hunter"), + ... Task(id="2", title="Test", assigned_to="test-coverage"), + ... Task(id="3", title="Fix", assigned_to="bug-hunter") + ... ] + >>> get_agent_workload(tasks) + {"bug-hunter": 2, "test-coverage": 1} + """ + workload = {} + for task in tasks: + if task.assigned_to: + workload[task.assigned_to] = workload.get(task.assigned_to, 0) + 1 + return workload + + +def suggest_agent_for_domain(domain: str, available_agents: list[str]) -> str: + """Suggest an agent based on a domain keyword. + + Quick lookup function for finding agents that specialize in + particular domains like "testing", "performance", etc. + + Args: + domain: Domain keyword (e.g., "testing", "database", "api") + available_agents: List of available agent names + + Returns: + Suggested agent name or default if no match found + + Example: + >>> suggest_agent_for_domain("testing", ["test-coverage", "bug-hunter"]) + "test-coverage" + """ + domain_lower = domain.lower() + agent_capabilities = _filter_available_agents(available_agents) + + # Find agents with matching domains + matches = [] + for agent_name, config in agent_capabilities.items(): + if domain_lower in [d.lower() for d in config.get("domains", [])]: + matches.append((agent_name, config.get("priority", 5))) + + if matches: + # Return highest priority match + matches.sort(key=lambda x: x[1], reverse=True) + return matches[0][0] + + # Fallback to default or first available + if DEFAULT_AGENT in available_agents: + return DEFAULT_AGENT + return available_agents[0] if available_agents else DEFAULT_AGENT + + +__all__ = ["assign_agent", "get_agent_workload", "suggest_agent_for_domain"] diff --git a/amplifier/planner/contracts/README.md b/amplifier/planner/contracts/README.md new file mode 100644 index 00000000..44cecc56 --- /dev/null +++ b/amplifier/planner/contracts/README.md @@ -0,0 +1,203 @@ +# Super-Planner API Contracts + +This directory contains the comprehensive API contracts for the super-planner system, following amplifier's "bricks and studs" philosophy. + +## Overview + +The contracts defined here represent the stable "studs" (connection points) that enable the super-planner to be regenerated internally without breaking external integrations. These contracts are the promises we make to consumers of the planner API. + +## Contract Files + +### 1. `__init__.py` - Core Python API Contracts +- **Data Models**: `Task`, `Project`, `TaskStatus`, `ProjectStatus`, `AgentType` +- **Module Protocols**: `CoreModule`, `PlanningMode`, `WorkingMode`, `PersistenceLayer`, `EventSystem` +- **Integration Protocols**: `AmplifierIntegration`, `LLMService` +- **Event Schemas**: `TaskEvent`, `AgentEvent` +- **Factory Functions**: `create_planner()`, `load_planner()` + +### 2. `openapi.yaml` - REST API Specification +- Complete OpenAPI 3.0 specification +- RESTful endpoints for projects, tasks, planning, orchestration +- Server-sent events (SSE) for real-time updates +- Standard error response formats +- Request/response schemas + +### 3. `integration.md` - Ecosystem Integration +- CLI integration via make commands +- Task tool integration for agent spawning +- CCSDK toolkit integration with defensive utilities +- Git workflow integration +- Event system contracts +- Extension points for customization + +### 4. `validation.py` - Contract Validation +- `ContractValidator`: Runtime validation of protocol implementations +- `ContractTest`: Base class for contract-based testing +- `ContractMonitor`: Production monitoring of contract compliance +- Validation utilities for data models, serialization, and API responses + +## Design Principles + +### 1. Ruthless Simplicity +Every endpoint and method has a single, clear purpose. No unnecessary complexity or premature abstraction. + +### 2. Stable Connection Points +The external contracts (public API) remain stable even as internal implementations change. This enables module regeneration without breaking consumers. + +### 3. Git-Friendly Persistence +All data serialization uses sorted JSON keys for clean git diffs and version control integration. + +### 4. Defensive by Default +Integration with CCSDK defensive utilities for robust LLM interaction and error handling. + +## Usage Examples + +### Python Module Usage +```python +from amplifier.planner import create_planner + +# Create planner with configuration +planner = await create_planner({ + "persistence_dir": "data/projects", + "git_enabled": True, + "max_concurrent_agents": 5 +}) + +# Create and plan project +project = await planner.create_project( + name="API Redesign", + goal="Redesign user API following REST principles" +) + +# Decompose into tasks +tasks = await planner.decompose_goal(project.goal) + +# Assign and execute +await planner.assign_tasks([t.id for t in tasks]) +await planner.execute_project(project.id) +``` + +### CLI Usage +```bash +# Create new project +make planner-new NAME="API Redesign" GOAL="Redesign user API" + +# Decompose into tasks +make planner-decompose PROJECT_ID="project-123" + +# Assign tasks to agents +make planner-assign PROJECT_ID="project-123" STRATEGY="optimal" + +# Execute with monitoring +make planner-execute PROJECT_ID="project-123" +make planner-monitor PROJECT_ID="project-123" +``` + +### REST API Usage +```bash +# Create project +curl -X POST http://localhost:8000/api/v1/projects \ + -H "Content-Type: application/json" \ + -d '{"name": "API Redesign", "goal": "Redesign user API"}' + +# Decompose goal +curl -X POST http://localhost:8000/api/v1/planning/decompose \ + -H "Content-Type: application/json" \ + -d '{"goal": "Redesign user API"}' + +# Stream events +curl -N http://localhost:8000/api/v1/events/stream?project_id=project-123 +``` + +## Contract Stability Guarantees + +### Stable (Won't Break) +- Public API methods in `__init__.py` +- OpenAPI endpoint paths and response schemas +- Core data model fields (`Task`, `Project`) +- Event type names and required fields +- CLI command interfaces + +### Evolvable (May Extend) +- Optional parameters and fields (additions okay) +- New endpoints (additions okay) +- New event types (additions okay) +- Internal module implementations (can be regenerated) +- Configuration options (new options with defaults okay) + +### Versioned Changes +- Breaking changes require major version bump (v1 -> v2) +- Deprecation notices maintained for 2 major versions +- Migration tools provided for data format changes + +## Testing Contracts + +### Unit Testing +```python +from amplifier.planner.contracts.validation import ContractTest + +class TestPlannerImplementation(ContractTest): + def test_core_module_contract(self): + core = MyCoreImplementation() + self.assert_implements_protocol(core, CoreModule) + + def test_task_serialization(self): + task = create_test_task() + self.assert_serializable(task, Task) +``` + +### Runtime Monitoring +```python +from amplifier.planner.contracts.validation import ContractMonitor + +monitor = ContractMonitor(logger) +monitor.check_protocol(my_implementation, PlanningMode) +monitor.check_data_model(task_instance, Task) + +# Get violation report +report = monitor.get_report() +``` + +## Extension Points + +The planner provides several extension points for customization without modifying core contracts: + +1. **Custom Agent Types**: Register new agent types with selection criteria +2. **Assignment Strategies**: Implement custom task assignment algorithms +3. **Persistence Backends**: Swap file-based storage for database or cloud +4. **Event Handlers**: Subscribe to events for custom workflows +5. **Planning Strategies**: Override decomposition algorithms + +## Integration with Amplifier Philosophy + +These contracts embody amplifier's core principles: + +- **Ruthless Simplicity**: Minimal, focused APIs without over-engineering +- **Bricks and Studs**: Clear module boundaries with stable connection points +- **Regenerability**: Internal modules can be rebuilt without breaking contracts +- **Present-Focus**: Solve current needs, not hypothetical futures +- **Trust in Emergence**: Let complex behavior emerge from simple, well-defined pieces + +## Compliance Checklist + +When implementing or modifying the planner: + +- [ ] All public methods match protocol signatures +- [ ] Data models serialize/deserialize correctly +- [ ] JSON uses sorted keys for git-friendliness +- [ ] API responses match OpenAPI schemas +- [ ] Events include required fields +- [ ] Error codes follow standard format +- [ ] Integration tests pass with mock implementations +- [ ] Contract validation tests pass +- [ ] Documentation updated for any contract changes + +## Next Steps + +1. Implement core modules following these contracts +2. Create contract tests for each module +3. Set up runtime contract monitoring +4. Build CLI and REST API layers +5. Integrate with amplifier's existing tools + +The contracts defined here provide the stable foundation for building a robust, regenerable task planning and orchestration system that fits seamlessly into the amplifier ecosystem. diff --git a/amplifier/planner/contracts/__init__.py b/amplifier/planner/contracts/__init__.py new file mode 100644 index 00000000..b0f1f14b --- /dev/null +++ b/amplifier/planner/contracts/__init__.py @@ -0,0 +1,526 @@ +""" +Super-Planner API Contracts + +This module defines the stable API contracts ("studs") for the super-planner system, +following amplifier's bricks-and-studs philosophy. These contracts enable module +regeneration without breaking external consumers. + +The contracts are organized into: +- External APIs: Public interfaces for amplifier ecosystem integration +- Internal contracts: Module boundaries within the planner +- Data exchange formats: Standardized data structures +- Event schemas: Notification and audit trail formats +""" + +import json +from collections.abc import AsyncIterator +from dataclasses import dataclass +from dataclasses import field +from datetime import datetime +from enum import Enum +from typing import Any +from typing import Optional +from typing import Protocol + +# ============================================================================ +# CORE DATA MODELS (Stable External Contracts) +# ============================================================================ + + +class TaskStatus(str, Enum): + """Task lifecycle states""" + + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + FAILED = "failed" + BLOCKED = "blocked" + + +class TaskType(str, Enum): + """Task classification""" + + GOAL = "goal" + TASK = "task" + SUBTASK = "subtask" + + +class ProjectStatus(str, Enum): + """Project lifecycle states""" + + ACTIVE = "active" + COMPLETED = "completed" + ARCHIVED = "archived" + + +class AgentType(str, Enum): + """Available agent types from amplifier ecosystem""" + + ZEN_ARCHITECT = "zen-architect" + MODULAR_BUILDER = "modular-builder" + TEST_COVERAGE = "test-coverage" + BUG_HUNTER = "bug-hunter" + REFACTOR_ARCHITECT = "refactor-architect" + INTEGRATION_SPECIALIST = "integration-specialist" + DATABASE_ARCHITECT = "database-architect" + API_CONTRACT_DESIGNER = "api-contract-designer" + CUSTOM = "custom" + + +@dataclass +class Task: + """Core task data model - stable external contract""" + + id: str + project_id: str + title: str + description: str + type: TaskType + status: TaskStatus + + # Relationships + parent_id: str | None = None + dependencies: list[str] = field(default_factory=list) + subtasks: list[str] = field(default_factory=list) + + # Assignment + assigned_to: str | None = None + suggested_agent: AgentType | None = None + + # Timing + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + started_at: datetime | None = None + completed_at: datetime | None = None + + # Results and metadata + result: dict[str, Any] | None = None + metadata: dict[str, Any] = field(default_factory=dict) + version: int = 1 + + def to_json(self) -> str: + """Serialize to JSON with sorted keys for git-friendly storage""" + data = { + "id": self.id, + "project_id": self.project_id, + "title": self.title, + "description": self.description, + "type": self.type.value, + "status": self.status.value, + "parent_id": self.parent_id, + "dependencies": sorted(self.dependencies), + "subtasks": sorted(self.subtasks), + "assigned_to": self.assigned_to, + "suggested_agent": self.suggested_agent.value if self.suggested_agent else None, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + "started_at": self.started_at.isoformat() if self.started_at else None, + "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "result": self.result, + "metadata": self.metadata, + "version": self.version, + } + return json.dumps(data, sort_keys=True, indent=2) + + @classmethod + def from_json(cls, data: str) -> "Task": + """Deserialize from JSON""" + obj = json.loads(data) + return cls( + id=obj["id"], + project_id=obj["project_id"], + title=obj["title"], + description=obj["description"], + type=TaskType(obj["type"]), + status=TaskStatus(obj["status"]), + parent_id=obj.get("parent_id"), + dependencies=obj.get("dependencies", []), + subtasks=obj.get("subtasks", []), + assigned_to=obj.get("assigned_to"), + suggested_agent=AgentType(obj["suggested_agent"]) if obj.get("suggested_agent") else None, + created_at=datetime.fromisoformat(obj["created_at"]), + updated_at=datetime.fromisoformat(obj["updated_at"]), + started_at=datetime.fromisoformat(obj["started_at"]) if obj.get("started_at") else None, + completed_at=datetime.fromisoformat(obj["completed_at"]) if obj.get("completed_at") else None, + result=obj.get("result"), + metadata=obj.get("metadata", {}), + version=obj.get("version", 1), + ) + + +@dataclass +class Project: + """Project data model - stable external contract""" + + id: str + name: str + description: str + goal: str + status: ProjectStatus + + # Timing + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + completed_at: datetime | None = None + + # Metadata + metadata: dict[str, Any] = field(default_factory=dict) + version: int = 1 + + def to_json(self) -> str: + """Serialize to JSON with sorted keys""" + data = { + "id": self.id, + "name": self.name, + "description": self.description, + "goal": self.goal, + "status": self.status.value, + "created_at": self.created_at.isoformat(), + "updated_at": self.updated_at.isoformat(), + "completed_at": self.completed_at.isoformat() if self.completed_at else None, + "metadata": self.metadata, + "version": self.version, + } + return json.dumps(data, sort_keys=True, indent=2) + + @classmethod + def from_json(cls, data: str) -> "Project": + """Deserialize from JSON""" + obj = json.loads(data) + return cls( + id=obj["id"], + name=obj["name"], + description=obj["description"], + goal=obj["goal"], + status=ProjectStatus(obj["status"]), + created_at=datetime.fromisoformat(obj["created_at"]), + updated_at=datetime.fromisoformat(obj["updated_at"]), + completed_at=datetime.fromisoformat(obj["completed_at"]) if obj.get("completed_at") else None, + metadata=obj.get("metadata", {}), + version=obj.get("version", 1), + ) + + +# ============================================================================ +# MODULE INTERFACES (Internal Contracts Between Planner Modules) +# ============================================================================ + + +class CoreModule(Protocol): + """Contract for the core task/project management module""" + + async def create_project(self, name: str, goal: str, description: str = "") -> Project: + """Create a new project""" + ... + + async def get_project(self, project_id: str) -> Project | None: + """Retrieve project by ID""" + ... + + async def update_project(self, project_id: str, **updates) -> Project: + """Update project with optimistic locking""" + ... + + async def create_task(self, project_id: str, title: str, **kwargs) -> Task: + """Create a new task""" + ... + + async def get_task(self, task_id: str) -> Task | None: + """Retrieve task by ID""" + ... + + async def update_task(self, task_id: str, **updates) -> Task: + """Update task with optimistic locking""" + ... + + async def transition_task(self, task_id: str, new_status: TaskStatus, reason: str = "") -> Task: + """Transition task state with validation""" + ... + + async def list_tasks(self, project_id: str, **filters) -> list[Task]: + """List tasks with optional filtering""" + ... + + +class PlanningMode(Protocol): + """Contract for planning mode operations""" + + async def decompose_goal(self, goal: str, context: dict | None = None) -> list[Task]: + """Decompose high-level goal into tasks using LLM""" + ... + + async def refine_plan(self, tasks: list[Task], feedback: str) -> list[Task]: + """Refine task breakdown based on feedback""" + ... + + async def suggest_dependencies(self, tasks: list[Task]) -> dict[str, list[str]]: + """Analyze and suggest task dependencies""" + ... + + async def estimate_effort(self, task: Task) -> str: + """Estimate task effort (xs, s, m, l, xl)""" + ... + + +class WorkingMode(Protocol): + """Contract for working mode operations""" + + async def assign_tasks(self, task_ids: list[str], strategy: str = "optimal") -> dict[str, str]: + """Assign tasks to agents based on strategy""" + ... + + async def spawn_agent(self, task_id: str, agent_type: AgentType) -> str: + """Spawn sub-agent for task execution""" + ... + + async def coordinate_agents(self, project_id: str) -> None: + """Orchestrate multi-agent execution""" + ... + + async def handle_deadlock(self, blocked_tasks: list[str]) -> None: + """Resolve dependency deadlocks""" + ... + + +class PersistenceLayer(Protocol): + """Contract for persistence operations""" + + async def save_project(self, project: Project) -> None: + """Persist project to storage""" + ... + + async def load_project(self, project_id: str) -> Project | None: + """Load project from storage""" + ... + + async def save_task(self, task: Task) -> None: + """Persist task to storage""" + ... + + async def load_task(self, task_id: str) -> Task | None: + """Load task from storage""" + ... + + async def list_all_tasks(self, project_id: str) -> list[Task]: + """Load all tasks for a project""" + ... + + async def create_snapshot(self, project_id: str) -> str: + """Create git snapshot, return commit hash""" + ... + + async def restore_snapshot(self, project_id: str, commit_hash: str) -> None: + """Restore from git snapshot""" + ... + + +class EventSystem(Protocol): + """Contract for event publishing and audit trail""" + + async def emit_event(self, event_type: str, data: dict[str, Any]) -> None: + """Publish event for subscribers""" + ... + + async def subscribe(self, event_types: list[str]) -> AsyncIterator[dict]: + """Subscribe to event stream""" + ... + + async def get_audit_trail(self, entity_id: str) -> list[dict]: + """Retrieve audit trail for entity""" + ... + + +# ============================================================================ +# EXTERNAL INTEGRATION CONTRACTS +# ============================================================================ + + +class AmplifierIntegration(Protocol): + """Contract for integration with amplifier ecosystem""" + + async def spawn_task_agent(self, agent_type: str, prompt: str, context: dict) -> dict: + """Spawn sub-agent using amplifier's Task tool""" + ... + + async def get_ccsdk_session(self) -> Any: + """Get CCSDK SessionManager instance""" + ... + + async def call_defensive_utility(self, utility: str, **kwargs) -> Any: + """Call CCSDK defensive utility""" + ... + + +class LLMService(Protocol): + """Contract for LLM service integration""" + + async def generate(self, prompt: str, system_prompt: str = "", **kwargs) -> str: + """Generate LLM response""" + ... + + async def parse_json_response(self, response: str) -> dict: + """Parse JSON from LLM response using defensive utilities""" + ... + + +# ============================================================================ +# EVENT SCHEMAS +# ============================================================================ + + +@dataclass +class TaskEvent: + """Standard event format for task changes""" + + event_type: str # task_created, task_updated, task_completed, etc. + task_id: str + project_id: str + timestamp: datetime + actor: str | None = None # User or agent ID + changes: dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) + + def to_json(self) -> str: + """Serialize to JSON""" + return json.dumps( + { + "event_type": self.event_type, + "task_id": self.task_id, + "project_id": self.project_id, + "timestamp": self.timestamp.isoformat(), + "actor": self.actor, + "changes": self.changes, + "metadata": self.metadata, + }, + sort_keys=True, + ) + + +@dataclass +class AgentEvent: + """Standard event format for agent coordination""" + + event_type: str # agent_spawned, agent_completed, agent_failed + agent_id: str + agent_type: str + task_id: str | None + timestamp: datetime + status: str + result: dict[str, Any] | None = None + error: str | None = None + + def to_json(self) -> str: + """Serialize to JSON""" + return json.dumps( + { + "event_type": self.event_type, + "agent_id": self.agent_id, + "agent_type": self.agent_type, + "task_id": self.task_id, + "timestamp": self.timestamp.isoformat(), + "status": self.status, + "result": self.result, + "error": self.error, + }, + sort_keys=True, + ) + + +# ============================================================================ +# PUBLIC API EXPORTS (The "Studs" for External Use) +# ============================================================================ + +__all__ = [ + # Core data models + "Task", + "Project", + "TaskStatus", + "TaskType", + "ProjectStatus", + "AgentType", + # Module interfaces + "CoreModule", + "PlanningMode", + "WorkingMode", + "PersistenceLayer", + "EventSystem", + # Integration contracts + "AmplifierIntegration", + "LLMService", + # Event schemas + "TaskEvent", + "AgentEvent", +] + + +# ============================================================================ +# PYTHON MODULE INTERFACE (For Direct Import) +# ============================================================================ + + +async def create_planner(config: dict[str, Any] | None = None): + """ + Factory function to create configured planner instance. + This is the main entry point for Python code using the planner. + + Usage: + from amplifier.planner import create_planner + + planner = await create_planner({ + "persistence_dir": "/path/to/data", + "git_enabled": True, + "max_concurrent_agents": 5 + }) + + project = await planner.create_project("My Project", "Build awesome feature") + tasks = await planner.decompose_goal(project.goal) + """ + # Implementation will be in the main module + from amplifier.planner.main import Planner + + return await Planner.create(config or {}) + + +async def load_planner(project_id: str, config: dict[str, Any] | None = None): + """ + Load existing planner with project. + + Usage: + planner = await load_planner("project-123") + tasks = await planner.list_tasks() + """ + from amplifier.planner.main import Planner + + planner = await Planner.create(config or {}) + await planner.load_project(project_id) + return planner + + +# ============================================================================ +# CLI INTERFACE CONTRACT +# ============================================================================ + + +class CLICommands: + """ + Defines the CLI interface for make commands. + These map to the main entry points in __main__.py + """ + + # Project commands + PLANNER_NEW = "planner-new" # Create new project + PLANNER_LOAD = "planner-load" # Load existing project + PLANNER_STATUS = "planner-status" # Show project status + + # Planning mode + PLANNER_DECOMPOSE = "planner-decompose" # Decompose goal into tasks + PLANNER_REFINE = "planner-refine" # Refine task breakdown + + # Working mode + PLANNER_ASSIGN = "planner-assign" # Assign tasks to agents + PLANNER_EXECUTE = "planner-execute" # Start execution + PLANNER_MONITOR = "planner-monitor" # Monitor progress + + # Utilities + PLANNER_EXPORT = "planner-export" # Export project data + PLANNER_IMPORT = "planner-import" # Import project data diff --git a/amplifier/planner/contracts/integration.md b/amplifier/planner/contracts/integration.md new file mode 100644 index 00000000..ec11525a --- /dev/null +++ b/amplifier/planner/contracts/integration.md @@ -0,0 +1,522 @@ +# Super-Planner Integration Contracts + +This document specifies the integration contracts between the super-planner and the amplifier ecosystem, following the "bricks and studs" philosophy. + +## Overview + +The super-planner integrates with amplifier as a self-contained "brick" with well-defined "studs" (connection points). These contracts ensure the planner can be regenerated internally without breaking external integrations. + +## Integration Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Amplifier Ecosystem β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ CLI β”‚ β”‚ Task β”‚ β”‚ CCSDK β”‚ β”‚ +β”‚ β”‚Commands β”‚ β”‚ Tool β”‚ β”‚ Toolkit β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Super-Planner Public API β”‚ β”‚ +β”‚ β”‚ (Stable Contract Layer) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Internal Module Contracts β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Core β”‚ β”‚Modes β”‚ β”‚Persistβ”‚ β”‚Orch. β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## 1. CLI Integration Contract + +### Make Commands + +The planner exposes the following make commands in amplifier's Makefile: + +```makefile +# Project Management +planner-new: + @uv run python -m amplifier.planner new \ + --name "$(NAME)" \ + --goal "$(GOAL)" \ + --output "$(OUTPUT_DIR)" + +planner-load: + @uv run python -m amplifier.planner load \ + --project-id "$(PROJECT_ID)" + +planner-status: + @uv run python -m amplifier.planner status \ + --project-id "$(PROJECT_ID)" \ + --format "$(FORMAT)" # json, table, summary + +# Planning Mode +planner-decompose: + @uv run python -m amplifier.planner decompose \ + --project-id "$(PROJECT_ID)" \ + --max-depth "$(MAX_DEPTH)" \ + --max-tasks "$(MAX_TASKS)" + +planner-refine: + @uv run python -m amplifier.planner refine \ + --project-id "$(PROJECT_ID)" \ + --feedback "$(FEEDBACK)" + +# Working Mode +planner-assign: + @uv run python -m amplifier.planner assign \ + --project-id "$(PROJECT_ID)" \ + --strategy "$(STRATEGY)" # optimal, round_robin, load_balanced + +planner-execute: + @uv run python -m amplifier.planner execute \ + --project-id "$(PROJECT_ID)" \ + --max-concurrent "$(MAX_CONCURRENT)" + +planner-monitor: + @uv run python -m amplifier.planner monitor \ + --project-id "$(PROJECT_ID)" \ + --follow +``` + +### Python Module Usage + +Direct Python integration: + +```python +from amplifier.planner import create_planner, load_planner + +# Create new planner +planner = await create_planner({ + "persistence_dir": "data/projects", + "git_enabled": True +}) + +# Create and decompose project +project = await planner.create_project( + name="API Redesign", + goal="Redesign user API following REST principles" +) + +tasks = await planner.decompose_goal(project.goal) + +# Assign and execute +assignments = await planner.assign_tasks([t.id for t in tasks]) +await planner.execute_project(project.id) +``` + +## 2. Task Tool Integration Contract + +### Spawning Sub-Agents + +The planner uses amplifier's Task tool to spawn specialized agents: + +```python +# Contract for agent spawning +async def spawn_agent(task_id: str, agent_type: str) -> Dict: + """ + Spawn sub-agent using amplifier's Task tool. + + Returns: + { + "agent_id": "agent-123", + "status": "spawning", + "task_id": "task-456", + "result_channel": "channel-789" + } + """ + + # Build agent prompt from task + task = await self.get_task(task_id) + prompt = self._build_agent_prompt(task) + + # Call amplifier's Task tool + result = await self.amplifier.spawn_task_agent( + agent_type=agent_type, + prompt=prompt, + context={ + "task_id": task_id, + "project_id": task.project_id, + "dependencies": task.dependencies, + "working_directory": self.get_working_dir(task.project_id) + } + ) + + return result +``` + +### Agent Types Mapping + +```python +AGENT_TYPE_MAPPING = { + "architecture": "zen-architect", + "implementation": "modular-builder", + "testing": "test-coverage", + "debugging": "bug-hunter", + "refactoring": "refactor-architect", + "integration": "integration-specialist", + "database": "database-architect", + "api": "api-contract-designer" +} +``` + +## 3. CCSDK Integration Contract + +### SessionManager Integration + +```python +from amplifier.ccsdk_toolkit import SessionManager, ClaudeSession + +class PlannerSession: + """Integrates with CCSDK SessionManager for LLM operations""" + + def __init__(self): + self.session_manager = SessionManager() + self.claude_session = None + + async def initialize(self): + """Initialize Claude session with defensive utilities""" + self.claude_session = await self.session_manager.create_session( + model="claude-3-opus-20240229", + use_defensive=True + ) + + async def decompose_with_llm(self, goal: str) -> List[Dict]: + """Use CCSDK defensive utilities for robust JSON parsing""" + from amplifier.ccsdk_toolkit.defensive import parse_llm_json + + response = await self.claude_session.generate( + prompt=f"Decompose this goal into tasks: {goal}", + system_prompt=DECOMPOSITION_PROMPT + ) + + # Use defensive JSON parsing + tasks_data = parse_llm_json(response) + return tasks_data.get("tasks", []) +``` + +### Defensive Utilities Usage + +```python +from amplifier.ccsdk_toolkit.defensive import ( + parse_llm_json, + retry_with_feedback, + isolate_prompt +) + +class PlannerLLM: + """Uses CCSDK defensive utilities""" + + async def safe_decompose(self, goal: str) -> List[Task]: + """Decompose with retry and error recovery""" + + # Isolate user content from system prompts + clean_goal = isolate_prompt(goal) + + # Retry with feedback on failure + result = await retry_with_feedback( + async_func=self.decompose_goal, + prompt=clean_goal, + max_retries=3 + ) + + # Parse JSON safely + tasks_data = parse_llm_json(result) + return self._convert_to_tasks(tasks_data) +``` + +## 4. Git Integration Contract + +### File Structure + +``` +data/projects/ +β”œβ”€β”€ {project_id}/ +β”‚ β”œβ”€β”€ project.json # Project metadata +β”‚ β”œβ”€β”€ tasks/ +β”‚ β”‚ β”œβ”€β”€ {task_id}.json # Individual task files +β”‚ β”‚ └── index.json # Task index +β”‚ β”œβ”€β”€ assignments/ +β”‚ β”‚ └── index.json # Agent assignments +β”‚ β”œβ”€β”€ events/ +β”‚ β”‚ └── {date}.jsonl # Event log files +β”‚ └── .git/ # Git repository +``` + +### Git Operations + +```python +class GitPersistence: + """Git integration for version control""" + + async def save_with_commit(self, project_id: str, message: str): + """Save and commit changes""" + # Save all JSON with sorted keys + await self.save_project(project) + await self.save_all_tasks(tasks) + + # Git operations + await self.run_command(f"git add -A", cwd=project_dir) + await self.run_command( + f'git commit -m "{message}"', + cwd=project_dir + ) + + async def create_checkpoint(self, project_id: str) -> str: + """Create git tag for checkpoint""" + timestamp = datetime.now().isoformat() + tag = f"checkpoint-{timestamp}" + await self.run_command( + f'git tag {tag}', + cwd=self.get_project_dir(project_id) + ) + return tag +``` + +## 5. Event System Contract + +### Event Publication + +```python +class EventPublisher: + """Publishes events for external consumers""" + + async def emit(self, event_type: str, data: Dict): + """Emit event to subscribers""" + event = { + "event_type": event_type, + "timestamp": datetime.now().isoformat(), + "data": data + } + + # Write to event log + await self.append_to_log(event) + + # Notify SSE subscribers if configured + if self.sse_enabled: + await self.sse_manager.broadcast(event) +``` + +### Standard Event Types + +```python +EVENT_TYPES = { + # Task events + "task.created": "New task created", + "task.updated": "Task updated", + "task.started": "Task execution started", + "task.completed": "Task completed successfully", + "task.failed": "Task execution failed", + "task.blocked": "Task blocked by dependencies", + + # Agent events + "agent.spawned": "Sub-agent spawned", + "agent.assigned": "Agent assigned to task", + "agent.completed": "Agent completed task", + "agent.failed": "Agent failed", + + # Project events + "project.created": "Project created", + "project.updated": "Project updated", + "project.completed": "All tasks completed", + + # Planning events + "planning.started": "Decomposition started", + "planning.completed": "Decomposition completed", + "planning.refined": "Plan refined" +} +``` + +## 6. Extension Points Contract + +### Custom Agent Types + +```python +class AgentRegistry: + """Registry for custom agent types""" + + def register_agent_type( + self, + name: str, + selector: Callable[[Task], bool], + spawner: Callable[[Task], Dict] + ): + """Register custom agent type""" + self.custom_agents[name] = { + "selector": selector, # Function to match tasks + "spawner": spawner # Function to spawn agent + } +``` + +### Custom Assignment Strategies + +```python +class AssignmentStrategy(Protocol): + """Protocol for custom assignment strategies""" + + async def assign( + self, + tasks: List[Task], + available_agents: List[str] + ) -> Dict[str, str]: + """Return task_id -> agent_id mapping""" + ... + +# Register custom strategy +planner.register_strategy("my_strategy", MyCustomStrategy()) +``` + +### Custom Persistence Backends + +```python +class PersistenceBackend(Protocol): + """Protocol for custom persistence""" + + async def save(self, key: str, data: str) -> None: + """Save data with key""" + ... + + async def load(self, key: str) -> Optional[str]: + """Load data by key""" + ... + + async def list(self, prefix: str) -> List[str]: + """List keys with prefix""" + ... +``` + +## 7. Resource Management Contract + +### Concurrency Control + +```python +class ResourceManager: + """Manages concurrent agent execution""" + + MAX_CONCURRENT_AGENTS = 5 + MAX_MEMORY_PER_AGENT = "1GB" + MAX_TIME_PER_TASK = 300 # seconds + + async def acquire_slot(self, agent_id: str) -> bool: + """Acquire execution slot for agent""" + if self.active_count < self.MAX_CONCURRENT_AGENTS: + self.active_agents[agent_id] = datetime.now() + return True + return False + + async def release_slot(self, agent_id: str): + """Release execution slot""" + self.active_agents.pop(agent_id, None) +``` + +## 8. Error Handling Contract + +### Standard Error Codes + +```python +class PlannerError(Exception): + """Base planner exception""" + code: str + message: str + details: Dict[str, Any] + +ERROR_CODES = { + "PROJECT_NOT_FOUND": "Project does not exist", + "TASK_NOT_FOUND": "Task does not exist", + "INVALID_TRANSITION": "Invalid state transition", + "AGENT_SPAWN_FAILED": "Failed to spawn agent", + "DEPENDENCY_CYCLE": "Circular dependency detected", + "RESOURCE_EXHAUSTED": "No available execution slots", + "PERSISTENCE_ERROR": "Failed to save/load data", + "GIT_ERROR": "Git operation failed", + "LLM_ERROR": "LLM generation failed" +} +``` + +## 9. Configuration Contract + +### Configuration Schema + +```yaml +# planner.config.yaml +persistence: + type: file # file, database, memory + directory: data/projects + git_enabled: true + auto_commit: true + +orchestration: + max_concurrent_agents: 5 + agent_timeout: 300 + retry_failed_tasks: true + deadlock_timeout: 60 + +planning: + max_decomposition_depth: 3 + max_tasks_per_goal: 50 + use_defensive_parsing: true + +llm: + model: claude-3-opus-20240229 + temperature: 0.7 + max_retries: 3 + +monitoring: + emit_events: true + enable_sse: false + log_level: info +``` + +## 10. Testing Contract + +### Mock Implementations + +```python +class MockAmplifierIntegration: + """Mock for testing without real amplifier""" + + async def spawn_task_agent(self, agent_type: str, prompt: str, context: Dict): + return { + "agent_id": f"mock-agent-{uuid.uuid4()}", + "status": "ready", + "task_id": context["task_id"] + } + +class MockLLMService: + """Mock LLM for deterministic testing""" + + async def generate(self, prompt: str, **kwargs): + return '{"tasks": [{"title": "Mock task", "type": "task"}]}' +``` + +## Contract Stability Guarantees + +1. **Public API Stability**: The contracts defined in `contracts/__init__.py` are stable and will not break without major version change + +2. **Internal Module Boundaries**: Internal contracts between modules can evolve but must maintain protocol compatibility + +3. **Data Format Stability**: JSON serialization formats are stable for git-friendly storage + +4. **Event Schema Stability**: Event formats are append-only (new fields okay, removal requires version bump) + +5. **Configuration Compatibility**: New config options are optional with defaults, removal requires migration + +## Migration Path + +When contracts need to evolve: + +1. **Minor changes**: Add optional fields, new endpoints, additional event types +2. **Major changes**: Create v2 contracts alongside v1, support both during transition +3. **Deprecation**: Mark old contracts deprecated, maintain for 2 major versions +4. **Migration tools**: Provide automated migration for data format changes + +--- + +This integration contract ensures the super-planner can be regenerated internally while maintaining stable connections to the amplifier ecosystem. The "studs" defined here won't change even as the internal "brick" implementation evolves. diff --git a/amplifier/planner/contracts/openapi.yaml b/amplifier/planner/contracts/openapi.yaml new file mode 100644 index 00000000..0a733a44 --- /dev/null +++ b/amplifier/planner/contracts/openapi.yaml @@ -0,0 +1,726 @@ +openapi: 3.0.0 +info: + title: Super-Planner API + description: | + Task planning and orchestration system for AI-driven development. + + This API defines the stable connection points ("studs") for the super-planner + system following the amplifier bricks-and-studs philosophy. Each endpoint + represents a minimal, clear contract that enables module regeneration without + breaking external consumers. + + The API is organized into logical resource groups that map to internal modules + while maintaining stable external contracts. + version: 1.0.0 + +servers: + - url: /api/v1 + description: API v1 endpoint + +paths: + # Project Management + /projects: + post: + summary: Create new project + operationId: createProject + tags: [Projects] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectCreate' + responses: + '201': + description: Project created + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '400': + $ref: '#/components/responses/BadRequest' + + get: + summary: List all projects + operationId: listProjects + tags: [Projects] + parameters: + - name: status + in: query + schema: + type: string + enum: [active, completed, archived] + - name: limit + in: query + schema: + type: integer + default: 50 + minimum: 1 + maximum: 100 + - name: offset + in: query + schema: + type: integer + default: 0 + minimum: 0 + responses: + '200': + description: Projects retrieved + content: + application/json: + schema: + type: object + required: [items, total] + properties: + items: + type: array + items: + $ref: '#/components/schemas/Project' + total: + type: integer + + /projects/{project_id}: + get: + summary: Get project details + operationId: getProject + tags: [Projects] + parameters: + - $ref: '#/components/parameters/ProjectId' + responses: + '200': + description: Project retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '404': + $ref: '#/components/responses/NotFound' + + patch: + summary: Update project + operationId: updateProject + tags: [Projects] + parameters: + - $ref: '#/components/parameters/ProjectId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectUpdate' + responses: + '200': + description: Project updated + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + + delete: + summary: Archive project + operationId: archiveProject + tags: [Projects] + parameters: + - $ref: '#/components/parameters/ProjectId' + responses: + '204': + description: Project archived + '404': + $ref: '#/components/responses/NotFound' + + # Task Management + /projects/{project_id}/tasks: + post: + summary: Create task + operationId: createTask + tags: [Tasks] + parameters: + - $ref: '#/components/parameters/ProjectId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TaskCreate' + responses: + '201': + description: Task created + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + + get: + summary: List project tasks + operationId: listTasks + tags: [Tasks] + parameters: + - $ref: '#/components/parameters/ProjectId' + - name: status + in: query + schema: + type: string + enum: [pending, in_progress, completed, failed, blocked] + - name: assigned_to + in: query + schema: + type: string + - name: parent_id + in: query + schema: + type: string + responses: + '200': + description: Tasks retrieved + content: + application/json: + schema: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/Task' + + /tasks/{task_id}: + get: + summary: Get task details + operationId: getTask + tags: [Tasks] + parameters: + - $ref: '#/components/parameters/TaskId' + responses: + '200': + description: Task retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + '404': + $ref: '#/components/responses/NotFound' + + patch: + summary: Update task + operationId: updateTask + tags: [Tasks] + parameters: + - $ref: '#/components/parameters/TaskId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TaskUpdate' + responses: + '200': + description: Task updated + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + + /tasks/{task_id}/transition: + post: + summary: Transition task state + operationId: transitionTask + tags: [Tasks] + parameters: + - $ref: '#/components/parameters/TaskId' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [state] + properties: + state: + type: string + enum: [pending, in_progress, completed, failed, blocked] + reason: + type: string + description: Optional reason for transition + responses: + '200': + description: Task transitioned + content: + application/json: + schema: + $ref: '#/components/schemas/Task' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + + # Planning Mode Operations + /planning/decompose: + post: + summary: Decompose goal into tasks + operationId: decomposeGoal + tags: [Planning] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [goal] + properties: + goal: + type: string + description: High-level goal to decompose + context: + type: object + description: Additional context for planning + constraints: + type: object + properties: + max_depth: + type: integer + default: 3 + max_tasks: + type: integer + default: 50 + responses: + '200': + description: Decomposition completed + content: + application/json: + schema: + type: object + required: [tasks, dependencies] + properties: + tasks: + type: array + items: + $ref: '#/components/schemas/PlannedTask' + dependencies: + type: array + items: + type: object + properties: + from_task_id: + type: string + to_task_id: + type: string + type: + type: string + enum: [blocks, requires] + + /planning/refine: + post: + summary: Refine task breakdown + operationId: refineTasks + tags: [Planning] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [tasks] + properties: + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + feedback: + type: string + description: User feedback on current plan + responses: + '200': + description: Refinement completed + content: + application/json: + schema: + type: object + properties: + tasks: + type: array + items: + $ref: '#/components/schemas/Task' + changes: + type: array + items: + type: object + properties: + task_id: + type: string + change_type: + type: string + enum: [added, modified, removed] + description: + type: string + + # Working Mode Operations + /orchestration/assign: + post: + summary: Assign tasks to agents + operationId: assignTasks + tags: [Orchestration] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [task_ids] + properties: + task_ids: + type: array + items: + type: string + strategy: + type: string + enum: [optimal, round_robin, load_balanced] + default: optimal + constraints: + type: object + properties: + max_concurrent: + type: integer + default: 5 + agent_types: + type: array + items: + type: string + responses: + '200': + description: Tasks assigned + content: + application/json: + schema: + type: object + properties: + assignments: + type: array + items: + type: object + properties: + task_id: + type: string + agent_id: + type: string + agent_type: + type: string + estimated_duration: + type: integer + + /orchestration/spawn: + post: + summary: Spawn agent for task + operationId: spawnAgent + tags: [Orchestration] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [task_id, agent_type] + properties: + task_id: + type: string + agent_type: + type: string + config: + type: object + description: Agent-specific configuration + responses: + '202': + description: Agent spawn initiated + content: + application/json: + schema: + type: object + properties: + agent_id: + type: string + status: + type: string + enum: [spawning, ready] + task_id: + type: string + + /orchestration/status: + get: + summary: Get orchestration status + operationId: getOrchestrationStatus + tags: [Orchestration] + parameters: + - name: project_id + in: query + schema: + type: string + responses: + '200': + description: Status retrieved + content: + application/json: + schema: + type: object + properties: + active_agents: + type: array + items: + type: object + properties: + agent_id: + type: string + agent_type: + type: string + task_id: + type: string + status: + type: string + started_at: + type: string + format: date-time + queued_tasks: + type: integer + completed_tasks: + type: integer + failed_tasks: + type: integer + + # Events and Notifications + /events/stream: + get: + summary: Stream events (SSE) + operationId: streamEvents + tags: [Events] + parameters: + - name: project_id + in: query + schema: + type: string + - name: task_id + in: query + schema: + type: string + - name: event_types + in: query + schema: + type: array + items: + type: string + enum: [task_created, task_updated, task_completed, agent_spawned, agent_completed] + responses: + '200': + description: Event stream + content: + text/event-stream: + schema: + type: string + +components: + parameters: + ProjectId: + name: project_id + in: path + required: true + schema: + type: string + + TaskId: + name: task_id + in: path + required: true + schema: + type: string + + schemas: + ProjectCreate: + type: object + required: [name, goal] + properties: + name: + type: string + minLength: 1 + maxLength: 100 + description: + type: string + maxLength: 500 + goal: + type: string + minLength: 1 + metadata: + type: object + + Project: + allOf: + - $ref: '#/components/schemas/ProjectCreate' + - type: object + required: [id, status, created_at, updated_at] + properties: + id: + type: string + status: + type: string + enum: [active, completed, archived] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + version: + type: integer + + ProjectUpdate: + type: object + properties: + name: + type: string + description: + type: string + status: + type: string + enum: [active, completed, archived] + metadata: + type: object + + TaskCreate: + type: object + required: [title, type] + properties: + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + type: + type: string + enum: [goal, task, subtask] + parent_id: + type: string + dependencies: + type: array + items: + type: string + estimated_effort: + type: string + enum: [xs, s, m, l, xl] + metadata: + type: object + + Task: + allOf: + - $ref: '#/components/schemas/TaskCreate' + - type: object + required: [id, status, created_at, updated_at] + properties: + id: + type: string + project_id: + type: string + status: + type: string + enum: [pending, in_progress, completed, failed, blocked] + assigned_to: + type: string + started_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + version: + type: integer + result: + type: object + + TaskUpdate: + type: object + properties: + title: + type: string + description: + type: string + status: + type: string + enum: [pending, in_progress, completed, failed, blocked] + assigned_to: + type: string + result: + type: object + metadata: + type: object + + PlannedTask: + type: object + required: [title, description, type] + properties: + title: + type: string + description: + type: string + type: + type: string + enum: [goal, task, subtask] + estimated_effort: + type: string + enum: [xs, s, m, l, xl] + suggested_agent: + type: string + rationale: + type: string + + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string + details: + type: object + + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Conflict: + description: Resource conflict + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/amplifier/planner/contracts/validation.py b/amplifier/planner/contracts/validation.py new file mode 100644 index 00000000..43e5da5b --- /dev/null +++ b/amplifier/planner/contracts/validation.py @@ -0,0 +1,326 @@ +""" +Contract Validation Module + +This module provides validation utilities to ensure implementations comply +with the defined API contracts. It enables contract-based testing and +runtime validation of module boundaries. +""" + +import inspect +import json +from dataclasses import fields +from dataclasses import is_dataclass +from typing import Any +from typing import Optional + + +class ContractValidator: + """ + Validates that implementations comply with defined contracts. + Used for both testing and runtime validation. + """ + + @staticmethod + def validate_protocol_implementation(implementation: Any, protocol: type) -> list[str]: + """ + Validate that a class implements all methods required by a protocol. + + Returns list of validation errors (empty if valid). + """ + errors = [] + + # Get all abstract methods from protocol + protocol_methods = { + name: method + for name, method in inspect.getmembers(protocol) + if not name.startswith("_") and callable(method) + } + + # Check each required method + for method_name, protocol_method in protocol_methods.items(): + if not hasattr(implementation, method_name): + errors.append(f"Missing required method: {method_name}") + continue + + impl_method = getattr(implementation, method_name) + if not callable(impl_method): + errors.append(f"Attribute {method_name} is not callable") + continue + + # Check method signature compatibility + protocol_sig = inspect.signature(protocol_method) + impl_sig = inspect.signature(impl_method) + + # Check parameters (excluding self) + protocol_params = list(protocol_sig.parameters.values())[1:] + impl_params = list(impl_sig.parameters.values())[1:] + + if len(protocol_params) != len(impl_params): + errors.append( + f"Method {method_name} has wrong number of parameters: " + f"expected {len(protocol_params)}, got {len(impl_params)}" + ) + + return errors + + @staticmethod + def validate_data_model(instance: Any, model_class: type) -> list[str]: + """ + Validate that an instance conforms to a data model. + """ + errors = [] + + if not isinstance(instance, model_class): + errors.append(f"Instance is not of type {model_class.__name__}") + return errors + + # Validate required fields for dataclasses + if is_dataclass(model_class): + for field in fields(model_class): + if not hasattr(instance, field.name): + errors.append(f"Missing required field: {field.name}") + continue + + value = getattr(instance, field.name) + + # Check None for non-optional fields + if value is None and not _is_optional(field.type): + errors.append(f"Non-optional field {field.name} is None") + + return errors + + @staticmethod + def validate_json_serialization(instance: Any, model_class: type) -> list[str]: + """ + Validate that an instance can be serialized and deserialized. + """ + errors = [] + + # Check for to_json method + if not hasattr(instance, "to_json"): + errors.append(f"{model_class.__name__} missing to_json method") + return errors + + # Try serialization + try: + json_str = instance.to_json() + json_data = json.loads(json_str) + + # Check for sorted keys (git-friendly requirement) + if json_str != json.dumps(json_data, sort_keys=True, indent=2): + errors.append("JSON not serialized with sorted keys") + except Exception as e: + errors.append(f"Serialization failed: {e}") + return errors + + # Check for from_json method + if not hasattr(model_class, "from_json"): + errors.append(f"{model_class.__name__} missing from_json method") + return errors + + # Try round-trip + try: + restored = model_class.from_json(json_str) + if not isinstance(restored, model_class): + errors.append("Deserialization returned wrong type") + except Exception as e: + errors.append(f"Deserialization failed: {e}") + + return errors + + @staticmethod + def validate_api_response(response: dict[str, Any], schema: dict[str, Any]) -> list[str]: + """ + Validate API response against OpenAPI schema. + """ + errors = [] + + # Check required fields + for field in schema.get("required", []): + if field not in response: + errors.append(f"Missing required field: {field}") + + # Check field types + for field, value in response.items(): + if field in schema.get("properties", {}): + field_schema = schema["properties"][field] + errors.extend(_validate_field(field, value, field_schema)) + + return errors + + @staticmethod + def validate_event_schema(event: dict[str, Any], event_type: str) -> list[str]: + """ + Validate event conforms to expected schema. + """ + errors = [] + + # Check standard event fields + required_fields = ["event_type", "timestamp"] + for field in required_fields: + if field not in event: + errors.append(f"Missing required event field: {field}") + + # Check event type + if event.get("event_type") != event_type: + errors.append(f"Event type mismatch: expected {event_type}, got {event.get('event_type')}") + + # Check timestamp format + if "timestamp" in event: + try: + from datetime import datetime + + datetime.fromisoformat(event["timestamp"]) + except (ValueError, TypeError): + errors.append("Invalid timestamp format") + + return errors + + +class ContractTest: + """ + Base class for contract-based testing. + Provides utilities for testing module contracts. + """ + + def assert_implements_protocol(self, implementation: Any, protocol: type): + """Assert that implementation satisfies protocol contract.""" + errors = ContractValidator.validate_protocol_implementation(implementation, protocol) + if errors: + raise AssertionError( + f"Contract violation for {protocol.__name__}:\n" + "\n".join(f" - {e}" for e in errors) + ) + + def assert_valid_data_model(self, instance: Any, model_class: type): + """Assert that instance is valid according to data model.""" + errors = ContractValidator.validate_data_model(instance, model_class) + if errors: + raise AssertionError(f"Invalid {model_class.__name__}:\n" + "\n".join(f" - {e}" for e in errors)) + + def assert_serializable(self, instance: Any, model_class: type): + """Assert that instance can be serialized and restored.""" + errors = ContractValidator.validate_json_serialization(instance, model_class) + if errors: + raise AssertionError("Serialization contract violation:\n" + "\n".join(f" - {e}" for e in errors)) + + def assert_valid_api_response(self, response: dict[str, Any], schema: dict[str, Any]): + """Assert API response matches schema.""" + errors = ContractValidator.validate_api_response(response, schema) + if errors: + raise AssertionError("API response contract violation:\n" + "\n".join(f" - {e}" for e in errors)) + + +class ContractMonitor: + """ + Runtime contract monitoring for production systems. + Logs contract violations without failing. + """ + + def __init__(self, logger=None): + self.logger = logger or self._get_default_logger() + self.violations = [] + + def check_protocol(self, implementation: Any, protocol: type) -> bool: + """Check protocol implementation, log violations.""" + errors = ContractValidator.validate_protocol_implementation(implementation, protocol) + + if errors: + self.log_violation(f"Protocol {protocol.__name__}", implementation.__class__.__name__, errors) + return False + return True + + def check_data_model(self, instance: Any, model_class: type) -> bool: + """Check data model validity, log violations.""" + errors = ContractValidator.validate_data_model(instance, model_class) + + if errors: + self.log_violation(f"Data model {model_class.__name__}", str(instance)[:100], errors) + return False + return True + + def log_violation(self, contract: str, context: str, errors: list[str]): + """Log contract violation.""" + violation = {"contract": contract, "context": context, "errors": errors} + self.violations.append(violation) + + if self.logger: + self.logger.warning( + f"Contract violation in {contract} for {context}:\n" + "\n".join(f" - {e}" for e in errors) + ) + + def get_report(self) -> dict[str, Any]: + """Get contract violation report.""" + return { + "total_violations": len(self.violations), + "violations_by_contract": self._group_by_contract(), + "recent_violations": self.violations[-10:], + } + + def _group_by_contract(self) -> dict[str, int]: + """Group violations by contract type.""" + grouped = {} + for violation in self.violations: + contract = violation["contract"] + grouped[contract] = grouped.get(contract, 0) + 1 + return grouped + + def _get_default_logger(self): + """Get default logger.""" + import logging + + return logging.getLogger(__name__) + + +# Helper functions + + +def _is_optional(type_hint) -> bool: + """Check if type hint is Optional.""" + return hasattr(type_hint, "__origin__") and type_hint.__origin__ is Optional + + +def _validate_field(field_name: str, value: Any, schema: dict[str, Any]) -> list[str]: + """Validate a single field against schema.""" + errors = [] + + # Check type + if "type" in schema: + expected_type = schema["type"] + type_map = { + "string": str, + "integer": int, + "number": (int, float), + "boolean": bool, + "array": list, + "object": dict, + } + + if expected_type in type_map: + expected_python_type = type_map[expected_type] + if not isinstance(value, expected_python_type): + errors.append( + f"Field {field_name} has wrong type: expected {expected_type}, got {type(value).__name__}" + ) + + # Check enum values + if "enum" in schema and value not in schema["enum"]: + errors.append(f"Field {field_name} has invalid value: {value} not in {schema['enum']}") + + # Check string constraints + if isinstance(value, str): + if "minLength" in schema and len(value) < schema["minLength"]: + errors.append(f"Field {field_name} too short: minimum {schema['minLength']}, got {len(value)}") + if "maxLength" in schema and len(value) > schema["maxLength"]: + errors.append(f"Field {field_name} too long: maximum {schema['maxLength']}, got {len(value)}") + + # Check array constraints + if isinstance(value, list) and "items" in schema: + item_schema = schema["items"] + for i, item in enumerate(value): + errors.extend(_validate_field(f"{field_name}[{i}]", item, item_schema)) + + return errors + + +# Export contract validation utilities +__all__ = ["ContractValidator", "ContractTest", "ContractMonitor"] diff --git a/amplifier/planner/data/planner/projects/django-blog.json b/amplifier/planner/data/planner/projects/django-blog.json new file mode 100644 index 00000000..8c367f76 --- /dev/null +++ b/amplifier/planner/data/planner/projects/django-blog.json @@ -0,0 +1,113 @@ +{ + "id": "django-blog", + "name": "Django Blog", + "created_at": "2025-10-07T11:54:42.814456", + "updated_at": "2025-10-07T11:54:42.814482", + "tasks": { + "setup": { + "id": "setup", + "title": "Project Setup", + "description": "Create Django project and configure settings", + "state": "completed", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814463", + "updated_at": "2025-10-07T11:54:42.814464" + }, + "models": { + "id": "models", + "title": "Database Models", + "description": "Create User, Post, Comment models", + "state": "completed", + "parent_id": null, + "depends_on": [ + "setup" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814466", + "updated_at": "2025-10-07T11:54:42.814467" + }, + "admin": { + "id": "admin", + "title": "Admin Interface", + "description": "Configure Django admin", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814468", + "updated_at": "2025-10-07T11:54:42.814468" + }, + "views": { + "id": "views", + "title": "Views", + "description": "Create list, detail, create views", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814470", + "updated_at": "2025-10-07T11:54:42.814470" + }, + "templates": { + "id": "templates", + "title": "Templates", + "description": "Design HTML templates", + "state": "completed", + "parent_id": null, + "depends_on": [ + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814471", + "updated_at": "2025-10-07T11:54:42.814472" + }, + "urls": { + "id": "urls", + "title": "URL Configuration", + "description": "Set up routing", + "state": "completed", + "parent_id": null, + "depends_on": [ + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814472", + "updated_at": "2025-10-07T11:54:42.814473" + }, + "tests": { + "id": "tests", + "title": "Tests", + "description": "Write unit tests", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models", + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814474", + "updated_at": "2025-10-07T11:54:42.814474" + }, + "deploy": { + "id": "deploy", + "title": "Deployment", + "description": "Deploy to production", + "state": "completed", + "parent_id": null, + "depends_on": [ + "templates", + "urls", + "tests" + ], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.814475", + "updated_at": "2025-10-07T11:54:42.814475" + } + } +} \ No newline at end of file diff --git a/amplifier/planner/data/planner/projects/persistence-test.json b/amplifier/planner/data/planner/projects/persistence-test.json new file mode 100644 index 00000000..afbb0065 --- /dev/null +++ b/amplifier/planner/data/planner/projects/persistence-test.json @@ -0,0 +1,32 @@ +{ + "id": "persistence-test", + "name": "Persistence Test", + "created_at": "2025-10-07T11:54:42.768935", + "updated_at": "2025-10-07T11:54:42.768944", + "tasks": { + "t1": { + "id": "t1", + "title": "Task 1", + "description": "First task", + "state": "completed", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T11:54:42.768939", + "updated_at": "2025-10-07T11:54:42.768939" + }, + "t2": { + "id": "t2", + "title": "Task 2", + "description": "", + "state": "pending", + "parent_id": null, + "depends_on": [ + "t1" + ], + "assigned_to": "test-user", + "created_at": "2025-10-07T11:54:42.768942", + "updated_at": "2025-10-07T11:54:42.768942" + } + } +} \ No newline at end of file diff --git a/amplifier/planner/decomposer.py b/amplifier/planner/decomposer.py new file mode 100644 index 00000000..8ec188bc --- /dev/null +++ b/amplifier/planner/decomposer.py @@ -0,0 +1,288 @@ +"""Task Decomposition Module for Super-Planner + +Purpose: Intelligently break down high-level goals into specific, actionable tasks using AI analysis. +This module provides a simple, self-contained interface for decomposing goals into hierarchical tasks. + +Contract: + Input: High-level goal string and project context + Output: List of actionable Task objects with dependencies + Behavior: Uses LLM to analyze goals and create structured task breakdowns + Side Effects: Logs decomposition progress + Dependencies: PydanticAI for LLM integration + +Example: + >>> from amplifier.planner import Project + >>> from amplifier.planner.decomposer import decompose_goal, ProjectContext + >>> + >>> context = ProjectContext( + ... project=Project(id="proj1", name="New Feature"), + ... max_depth=3 + ... ) + >>> tasks = await decompose_goal("Build user authentication system", context) + >>> print(f"Generated {len(tasks)} tasks") +""" + +import logging +import uuid +from dataclasses import dataclass + +from pydantic import BaseModel +from pydantic import Field +from pydantic_ai import Agent + +from amplifier.planner.models import Project +from amplifier.planner.models import Task +from amplifier.planner.models import TaskState + +# Set up logging +logger = logging.getLogger(__name__) + + +@dataclass +class ProjectContext: + """Context for task decomposition within a project.""" + + project: Project + max_depth: int = 3 # Maximum decomposition depth + min_tasks: int = 2 # Minimum tasks required from decomposition + parent_task: Task | None = None # Parent task if decomposing subtasks + + +class TaskDecomposition(BaseModel): + """LLM response model for task decomposition.""" + + tasks: list[dict] = Field(description="List of tasks with title, description, and dependencies") + + model_config = { + "json_schema_extra": { + "example": { + "tasks": [ + { + "title": "Design database schema", + "description": "Create tables for user authentication", + "depends_on_indices": [], + }, + { + "title": "Implement login endpoint", + "description": "Create API endpoint for user login", + "depends_on_indices": [0], + }, + ] + } + } + } + + +# Lazy-initialize the decomposition agent +_decomposer_agent = None + + +def _get_decomposer_agent(): + """Get or create the decomposer agent (lazy initialization).""" + global _decomposer_agent + if _decomposer_agent is None: + _decomposer_agent = Agent( + "claude-3-5-sonnet-20241022", + output_type=TaskDecomposition, + system_prompt=( + "You are a task decomposition expert. Break down high-level goals into " + "specific, actionable tasks. Each task should be concrete and achievable. " + "Identify dependencies between tasks. Focus on practical implementation steps." + ), + ) + return _decomposer_agent + + +async def decompose_goal(goal: str, context: ProjectContext) -> list[Task]: + """Decompose a high-level goal into actionable tasks. + + This function uses AI to intelligently break down a goal into specific tasks, + establishing proper dependencies and hierarchical structure. + + Args: + goal: High-level goal string to decompose + context: Project context including existing project and constraints + + Returns: + List of Task objects with proper IDs, descriptions, and dependencies + + Raises: + ValueError: If goal is empty or decomposition produces too few tasks + RuntimeError: If LLM decomposition fails + + Example: + >>> context = ProjectContext(project=project, max_depth=2) + >>> tasks = await decompose_goal("Add search functionality", context) + >>> for task in tasks: + ... print(f"- {task.title}") + """ + # Input validation + if not goal or not goal.strip(): + raise ValueError("Goal cannot be empty") + + logger.info(f"Decomposing goal: {goal[:100]}...") + + # Build prompt with context + prompt = _build_decomposition_prompt(goal, context) + + try: + # Get the agent and call LLM for decomposition + agent = _get_decomposer_agent() + result = await agent.run(prompt) + decomposition = result.output + + # Validate minimum tasks requirement + if len(decomposition.tasks) < context.min_tasks: + logger.warning( + f"Decomposition produced only {len(decomposition.tasks)} tasks, minimum is {context.min_tasks}" + ) + # Try once more with explicit instruction + enhanced_prompt = ( + f"{prompt}\n\nIMPORTANT: Generate at least {context.min_tasks} distinct, actionable tasks." + ) + result = await agent.run(enhanced_prompt) + decomposition = result.output + + if len(decomposition.tasks) < context.min_tasks: + raise ValueError(f"Could not generate minimum {context.min_tasks} tasks from goal") + + # Convert to Task objects + tasks = _convert_to_tasks(decomposition, context) + + logger.info(f"Successfully decomposed into {len(tasks)} tasks") + return tasks + + except Exception as e: + logger.error(f"Failed to decompose goal: {e}") + raise RuntimeError(f"Goal decomposition failed: {e}") from e + + +def _build_decomposition_prompt(goal: str, context: ProjectContext) -> str: + """Build the decomposition prompt with context.""" + prompt_parts = [ + f"Goal: {goal}", + f"Project: {context.project.name}", + ] + + if context.parent_task: + prompt_parts.append(f"Parent task: {context.parent_task.title}") + prompt_parts.append("Create subtasks for this parent task.") + + # Add existing tasks context if any + if context.project.tasks: + existing_titles = [task.title for task in context.project.tasks.values()][:5] + prompt_parts.append(f"Existing tasks in project: {', '.join(existing_titles)}") + + prompt_parts.append( + "\nBreak this down into specific, actionable tasks. " + "Each task should be concrete and independently achievable. " + "Identify dependencies between tasks using indices (0-based)." + ) + + return "\n".join(prompt_parts) + + +def _convert_to_tasks(decomposition: TaskDecomposition, context: ProjectContext) -> list[Task]: + """Convert LLM decomposition to Task objects with proper IDs and relationships.""" + tasks = [] + task_ids = [] # Track IDs for dependency mapping + + # Generate IDs first + for _ in decomposition.tasks: + task_ids.append(str(uuid.uuid4())) + + # Create Task objects + for i, task_data in enumerate(decomposition.tasks): + # Map dependency indices to actual task IDs + dependencies = [] + if "depends_on_indices" in task_data: + for dep_idx in task_data.get("depends_on_indices", []): + if 0 <= dep_idx < len(task_ids) and dep_idx != i: + dependencies.append(task_ids[dep_idx]) + + task = Task( + id=task_ids[i], + title=task_data.get("title", f"Task {i + 1}"), + description=task_data.get("description", ""), + state=TaskState.PENDING, + parent_id=context.parent_task.id if context.parent_task else None, + depends_on=dependencies, + ) + + tasks.append(task) + + logger.debug(f"Created task: {task.title} (deps: {len(dependencies)})") + + return tasks + + +async def decompose_recursively(goal: str, context: ProjectContext, current_depth: int = 0) -> list[Task]: + """Recursively decompose a goal into tasks up to max_depth. + + This function decomposes a goal and then recursively decomposes each + resulting task until reaching the maximum depth or tasks become atomic. + + Args: + goal: High-level goal to decompose + context: Project context with constraints + current_depth: Current recursion depth (internal) + + Returns: + Flat list of all tasks from all decomposition levels + + Example: + >>> context = ProjectContext(project=project, max_depth=2) + >>> all_tasks = await decompose_recursively("Build app", context) + >>> print(f"Total tasks across all levels: {len(all_tasks)}") + """ + if current_depth >= context.max_depth: + logger.debug(f"Reached maximum decomposition depth: {context.max_depth}") + return [] + + # Decompose current goal + tasks = await decompose_goal(goal, context) + all_tasks = tasks.copy() + + # Recursively decompose each task + for task in tasks: + # Skip tasks that seem atomic or too specific + if _is_atomic_task(task): + logger.debug(f"Skipping atomic task: {task.title}") + continue + + # Create context for subtask decomposition + sub_context = ProjectContext( + project=context.project, + max_depth=context.max_depth, + min_tasks=2, # Subtasks can have fewer requirements + parent_task=task, + ) + + try: + subtasks = await decompose_recursively(task.title, sub_context, current_depth + 1) + all_tasks.extend(subtasks) + except Exception as e: + logger.warning(f"Could not decompose '{task.title}': {e}") + # Continue with other tasks + + return all_tasks + + +def _is_atomic_task(task: Task) -> bool: + """Determine if a task is atomic and shouldn't be decomposed further.""" + # Simple heuristics for atomic tasks + atomic_keywords = [ + "write", + "create file", + "implement", + "add test", + "update", + "fix", + "remove", + "delete", + "install", + "configure", + ] + + title_lower = task.title.lower() + return any(keyword in title_lower for keyword in atomic_keywords) diff --git a/amplifier/planner/models.py b/amplifier/planner/models.py new file mode 100644 index 00000000..b1e0da5e --- /dev/null +++ b/amplifier/planner/models.py @@ -0,0 +1,60 @@ +"""Task and Project data models for the Super-Planner system.""" + +from dataclasses import dataclass +from dataclasses import field +from datetime import datetime +from enum import Enum + + +class TaskState(Enum): + """Task states for workflow management.""" + + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + BLOCKED = "blocked" + + +@dataclass +class Task: + """Task with hierarchical structure and dependencies.""" + + id: str + title: str + description: str = "" + state: TaskState = TaskState.PENDING + parent_id: str | None = None + depends_on: list[str] = field(default_factory=list) + assigned_to: str | None = None + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + + def can_start(self, completed_ids: set[str]) -> bool: + """Check if task can start based on dependency completion.""" + if not self.depends_on: + return True + return all(dep_id in completed_ids for dep_id in self.depends_on) + + +@dataclass +class Project: + """Project container for hierarchical tasks.""" + + id: str + name: str + tasks: dict[str, Task] = field(default_factory=dict) + created_at: datetime = field(default_factory=datetime.now) + updated_at: datetime = field(default_factory=datetime.now) + + def add_task(self, task: Task) -> None: + """Add task to project and update timestamp.""" + self.tasks[task.id] = task + self.updated_at = datetime.now() + + def get_roots(self) -> list[Task]: + """Get all root tasks (no parent).""" + return [task for task in self.tasks.values() if task.parent_id is None] + + def get_children(self, parent_id: str) -> list[Task]: + """Get direct children of a task.""" + return [task for task in self.tasks.values() if task.parent_id == parent_id] diff --git a/amplifier/planner/orchestrator.py b/amplifier/planner/orchestrator.py new file mode 100644 index 00000000..e9134630 --- /dev/null +++ b/amplifier/planner/orchestrator.py @@ -0,0 +1,264 @@ +"""Task execution orchestrator for the Super-Planner system. + +This module coordinates parallel agent execution while respecting task dependencies. +It follows the modular "bricks and studs" philosophy with a clear public contract. + +Public Contract: + orchestrate_execution(project: Project) -> ExecutionResults + + Purpose: Coordinate parallel execution of project tasks via agents + + Input: Project with tasks and dependencies + Output: ExecutionResults with status, progress, and outcomes + + Behavior: + - Resolves task dependencies to determine execution order + - Spawns agents in parallel where dependencies allow + - Tracks progress and handles failures gracefully + - Returns comprehensive execution results +""" + +import asyncio +import logging +from dataclasses import dataclass +from dataclasses import field +from datetime import datetime +from typing import Any + +from amplifier.planner.models import Project +from amplifier.planner.models import Task +from amplifier.planner.models import TaskState + +logger = logging.getLogger(__name__) + + +@dataclass +class TaskResult: + """Result from executing a single task.""" + + task_id: str + status: str # "success", "failed", "skipped" + output: Any = None + error: str | None = None + started_at: datetime = field(default_factory=datetime.now) + completed_at: datetime | None = None + attempts: int = 1 + + +@dataclass +class ExecutionResults: + """Results from orchestrating project execution.""" + + project_id: str + status: str # "completed", "partial", "failed" + task_results: dict[str, TaskResult] = field(default_factory=dict) + total_tasks: int = 0 + completed_tasks: int = 0 + failed_tasks: int = 0 + skipped_tasks: int = 0 + started_at: datetime = field(default_factory=datetime.now) + completed_at: datetime | None = None + + def add_result(self, result: TaskResult) -> None: + """Add a task result and update counters.""" + self.task_results[result.task_id] = result + + if result.status == "success": + self.completed_tasks += 1 + elif result.status == "failed": + self.failed_tasks += 1 + elif result.status == "skipped": + self.skipped_tasks += 1 + + def finalize(self) -> None: + """Finalize results and set overall status.""" + self.completed_at = datetime.now() + + if self.failed_tasks == 0 and self.completed_tasks == self.total_tasks: + self.status = "completed" + elif self.completed_tasks > 0: + self.status = "partial" + else: + self.status = "failed" + + +async def orchestrate_execution(project: Project, max_parallel: int = 5, max_retries: int = 2) -> ExecutionResults: + """Orchestrate parallel execution of project tasks. + + Args: + project: Project containing tasks to execute + max_parallel: Maximum number of agents to run in parallel + max_retries: Maximum retry attempts for failed tasks + + Returns: + ExecutionResults with comprehensive execution information + """ + results = ExecutionResults(project_id=project.id, status="in_progress", total_tasks=len(project.tasks)) + + if not project.tasks: + logger.warning(f"Project {project.id} has no tasks to execute") + results.status = "completed" + results.finalize() + return results + + # Track task states + completed_ids: set[str] = set() + in_progress_ids: set[str] = set() + failed_ids: set[str] = set() + queued_ids: set[str] = set() # Track what's been queued + + # Create execution queue + execution_queue = asyncio.Queue() + + # Start with tasks that have no dependencies + for task in project.tasks.values(): + if task.can_start(completed_ids): + await execution_queue.put(task) + queued_ids.add(task.id) + + # Semaphore to limit parallel execution + semaphore = asyncio.Semaphore(max_parallel) + + async def execute_task(task: Task) -> TaskResult: + """Execute a single task with retries.""" + result = TaskResult(task_id=task.id, status="failed") + + for attempt in range(1, max_retries + 1): + try: + async with semaphore: + logger.info(f"Executing task {task.id}: {task.title} (attempt {attempt})") + in_progress_ids.add(task.id) + + # Update task state + task.state = TaskState.IN_PROGRESS + task.updated_at = datetime.now() + + # Execute via agent (simplified for now - would integrate with Task tool) + output = await _execute_with_agent(task) + + # Success + result.status = "success" + result.output = output + result.attempts = attempt + result.completed_at = datetime.now() + + task.state = TaskState.COMPLETED + task.updated_at = datetime.now() + + in_progress_ids.discard(task.id) + completed_ids.add(task.id) + + logger.info(f"Task {task.id} completed successfully") + break + + except Exception as e: + logger.error(f"Task {task.id} failed (attempt {attempt}): {e}") + result.error = str(e) + result.attempts = attempt + + if attempt == max_retries: + result.status = "failed" + result.completed_at = datetime.now() + + task.state = TaskState.BLOCKED + task.updated_at = datetime.now() + + in_progress_ids.discard(task.id) + failed_ids.add(task.id) + else: + # Exponential backoff + await asyncio.sleep(2**attempt) + + return result + + async def process_queue(): + """Process tasks from the queue.""" + active_tasks = [] + + while execution_queue.qsize() > 0 or active_tasks: + # Start new tasks + while not execution_queue.empty() and len(active_tasks) < max_parallel: + try: + task = execution_queue.get_nowait() + active_tasks.append(asyncio.create_task(execute_task(task))) + except asyncio.QueueEmpty: + break + + # Wait for at least one task to complete + if active_tasks: + done, pending = await asyncio.wait(active_tasks, return_when=asyncio.FIRST_COMPLETED) + + # Process completed tasks + for task_future in done: + result = await task_future + results.add_result(result) + active_tasks.remove(task_future) + + # Check for newly unblocked tasks only after successful completion + if result.status == "success": + # Look for tasks that are now ready to run + for task_id, task in project.tasks.items(): + if ( + task_id not in queued_ids # Not already queued + and task.can_start(completed_ids) + ): + await execution_queue.put(task) + queued_ids.add(task_id) + + # Update active tasks list + active_tasks = list(pending) + + # Brief pause to avoid busy waiting + if execution_queue.empty() and active_tasks: + await asyncio.sleep(0.1) + + # Execute all tasks + await process_queue() + + # Handle any remaining unexecuted tasks (due to failed dependencies) + for task_id in project.tasks: + if task_id not in results.task_results: + result = TaskResult( + task_id=task_id, status="skipped", error="Dependencies failed or not met", completed_at=datetime.now() + ) + results.add_result(result) + logger.warning(f"Task {task_id} skipped due to unmet dependencies") + + # Finalize results + results.finalize() + + logger.info( + f"Orchestration complete for project {project.id}: " + f"{results.completed_tasks}/{results.total_tasks} completed, " + f"{results.failed_tasks} failed, {results.skipped_tasks} skipped" + ) + + return results + + +async def _execute_with_agent(task: Task) -> Any: + """Execute task with assigned agent. + + This is a simplified placeholder. In production, this would: + 1. Use the Task tool to spawn the appropriate agent + 2. Pass the task description and context + 3. Wait for and return the agent's output + + Args: + task: Task to execute + + Returns: + Agent execution output + """ + # Simulate agent execution + await asyncio.sleep(0.5) # Simulate work + + if task.assigned_to: + logger.debug(f"Would execute with agent: {task.assigned_to}") + return f"Executed by {task.assigned_to}: {task.title}" + logger.debug(f"Executing task without specific agent: {task.title}") + return f"Executed: {task.title}" + + +# Public exports +__all__ = ["orchestrate_execution", "ExecutionResults", "TaskResult"] diff --git a/amplifier/planner/protocols/README.md b/amplifier/planner/protocols/README.md new file mode 100644 index 00000000..39d7fbd7 --- /dev/null +++ b/amplifier/planner/protocols/README.md @@ -0,0 +1,287 @@ +# Task State Management and Coordination Protocols + +This document provides an overview of the comprehensive protocol system designed for the super-planner's task state management and multi-agent coordination capabilities. + +## Architecture Overview + +The protocol system consists of five core components that work together to ensure reliable, coordinated task management across multiple agents: + +1. **State Transition Protocol** - Manages valid task state changes +2. **Agent Coordination Protocol** - Handles multi-agent task claiming and load balancing +3. **Deadlock Prevention Protocol** - Detects and resolves circular dependencies +4. **Conflict Resolution Protocol** - Manages concurrent task modifications +5. **Defensive Coordination Utilities** - Provides robust error handling and recovery + +## Protocol Components + +### 1. State Transition Protocol (`state_transitions.py`) + +Manages atomic state transitions for tasks with optimistic locking and validation. + +**Key Features:** +- Valid transition definitions with guard conditions and side effects +- Optimistic locking with version numbers to prevent concurrent modification conflicts +- Immutable task updates with automatic version incrementing +- Comprehensive transition validation and error handling + +**State Flow:** +``` +NOT_STARTED β†’ ASSIGNED β†’ IN_PROGRESS β†’ COMPLETED + ↓ ↓ ↓ + IN_PROGRESS CANCELLED BLOCKED β†’ IN_PROGRESS + ↓ ↓ + COMPLETED CANCELLED +``` + +**Usage:** +```python +from amplifier.planner.protocols import state_protocol + +# Transition a task +updated_task = state_protocol.transition_task(task, TaskState.IN_PROGRESS, expected_version=task.version) + +# Check valid transitions +can_transition, reason = state_protocol.can_transition(task, TaskState.COMPLETED) +``` + +### 2. Agent Coordination Protocol (`agent_coordination.py`) + +Manages multi-agent coordination, task claiming, and load balancing to prevent conflicts. + +**Key Features:** +- Agent registration with capabilities and capacity limits +- Task claiming with time-based leases and automatic expiration +- Load balancing based on current agent utilization +- Heartbeat monitoring and automatic cleanup of inactive agents +- Capability-based task assignment matching + +**Core Operations:** +- `register_agent()` - Register agent with capabilities and limits +- `claim_task()` - Atomically claim a task with lease +- `release_task()` - Release a claimed task +- `select_best_agent()` - Load-balanced agent selection +- `cleanup_expired_claims()` - Remove expired leases + +**Usage:** +```python +from amplifier.planner.protocols import coordination_protocol + +# Register an agent +await coordination_protocol.register_agent("ai_agent_1", {"python", "analysis"}, max_concurrent_tasks=5) + +# Claim a task +claim = await coordination_protocol.claim_task("task_123", "ai_agent_1") + +# Get agent recommendations +best_agent = coordination_protocol.select_best_agent(task) +``` + +### 3. Deadlock Prevention Protocol (`deadlock_prevention.py`) + +Prevents and resolves deadlocks through cycle detection, dependency tracking, and automatic resolution. + +**Key Features:** +- Real-time circular dependency detection using graph algorithms +- Dependency depth limiting to prevent infinite chains +- Multiple deadlock resolution strategies (weakest link, most recent task) +- Timeout-based escalation for long-blocked tasks +- Comprehensive deadlock cycle analysis and severity assessment + +**Core Operations:** +- `add_dependency()` - Add dependency with cycle checking +- `detect_deadlocks()` - Find all active deadlock cycles +- `resolve_deadlock()` - Attempt automatic resolution +- `check_timeout_violations()` - Find overdue blocked tasks + +**Usage:** +```python +from amplifier.planner.protocols import deadlock_protocol + +# Add a dependency (with cycle prevention) +success = await deadlock_protocol.add_dependency("task_a", "task_b") + +# Check for deadlocks +cycles = await deadlock_protocol.detect_deadlocks() + +# Resolve if found +for cycle in cycles: + resolved = await deadlock_protocol.resolve_deadlock(cycle) +``` + +### 4. Conflict Resolution Protocol (`conflict_resolution.py`) + +Handles concurrent task modifications with intelligent merge strategies and escalation. + +**Key Features:** +- Multiple conflict detection types (version, state, assignment, dependency) +- Configurable resolution strategies (last writer wins, merge, escalate) +- Intelligent change merging for compatible modifications +- Automatic retry with exponential backoff +- Human escalation for complex conflicts + +**Resolution Strategies:** +- **Last Writer Wins** - Use most recent modification +- **First Writer Wins** - Use earliest modification +- **Merge Changes** - Intelligently combine non-conflicting changes +- **Escalate to Human** - Require manual intervention +- **Reject Conflict** - Block the conflicting change + +**Usage:** +```python +from amplifier.planner.protocols import conflict_protocol + +# Apply modification with automatic conflict resolution +resolved_task = await conflict_protocol.apply_modification_with_retry(current_task, modification) + +# Get conflicts needing human intervention +escalated = conflict_protocol.get_escalated_conflicts() +``` + +### 5. Defensive Coordination Utilities (`defensive_coordination.py`) + +Provides robust error handling, health monitoring, and graceful degradation capabilities. + +**Key Features:** +- Retry decorators with exponential backoff and configurable exceptions +- Defensive file I/O with cloud sync error handling (from DISCOVERIES.md patterns) +- Health monitoring for all coordination components +- Circuit breaker patterns for failing services +- Graceful degradation strategies when protocols fail + +**Core Utilities:** +- `@retry_with_backoff` - Decorator for automatic retries +- `DefensiveFileIO` - Cloud-sync aware file operations +- `CoordinationHealthCheck` - System health monitoring +- `TaskOperationContext` - Safe operation context manager +- `GracefulDegradation` - Fallback strategies + +**Usage:** +```python +from amplifier.planner.protocols import file_io, health_monitor, retry_with_backoff + +# Defensive file operations +file_io.write_json(data, Path("tasks.json")) + +# Health monitoring +health_status = await health_monitor.full_health_check() + +# Retry decorator +@retry_with_backoff() +async def risky_operation(): + # Operation that might fail + pass +``` + +## Integration Patterns + +### Task Lifecycle Management + +```python +from amplifier.planner.protocols import ( + state_protocol, + coordination_protocol, + deadlock_protocol, + conflict_protocol +) + +async def process_task_lifecycle(task): + # 1. Assign to agent + best_agent = coordination_protocol.select_best_agent(task) + claim = await coordination_protocol.claim_task(task.id, best_agent) + + # 2. Transition to assigned state + task = state_protocol.transition_task(task, TaskState.ASSIGNED) + + # 3. Check for dependency issues + cycles = await deadlock_protocol.detect_deadlocks() + if cycles: + for cycle in cycles: + await deadlock_protocol.resolve_deadlock(cycle) + + # 4. Start work + task = state_protocol.transition_task(task, TaskState.IN_PROGRESS) + + # 5. Handle concurrent modifications + if modifications: + task = await conflict_protocol.apply_modification_with_retry(task, modification) + + # 6. Complete and release + task = state_protocol.transition_task(task, TaskState.COMPLETED) + await coordination_protocol.release_task(task.id, best_agent) +``` + +### Health Monitoring and Recovery + +```python +from amplifier.planner.protocols import health_monitor, coordination_protocol, deadlock_protocol + +async def system_health_check(): + # Full system health check + health = await health_monitor.full_health_check() + + if health["overall_status"] == "unhealthy": + # Cleanup and recovery + await coordination_protocol.cleanup_expired_claims() + await coordination_protocol.cleanup_inactive_agents() + + # Check for deadlocks + violations = await deadlock_protocol.check_timeout_violations() + for task_id in violations: + # Escalate overdue tasks + pass +``` + +## Error Handling Strategy + +The protocol system uses a layered error handling approach: + +1. **Prevention** - Validate operations before execution +2. **Detection** - Monitor for conflicts, deadlocks, and failures +3. **Automatic Recovery** - Retry with backoff, resolve conflicts automatically +4. **Graceful Degradation** - Fallback strategies when protocols fail +5. **Human Escalation** - Manual intervention for complex cases + +## Configuration and Customization + +Each protocol supports configuration for different deployment scenarios: + +```python +# Custom retry configuration +retry_config = RetryConfig( + max_attempts=5, + initial_delay=1.0, + backoff_multiplier=2.0, + retry_on_exceptions=(ConnectionError, TimeoutError) +) + +# Custom deadlock thresholds +deadlock_protocol = DeadlockPreventionProtocol( + max_block_duration=timedelta(hours=2), + escalation_threshold=timedelta(minutes=30), + max_dependency_depth=5 +) + +# Custom conflict resolution strategies +conflict_protocol.resolution_strategies[ConflictType.ASSIGNMENT_CONFLICT] = ResolutionStrategy.LAST_WRITER_WINS +``` + +## Performance and Scalability + +The protocol system is designed for scalability: + +- **Lock-free where possible** - Uses optimistic locking instead of blocking +- **Asynchronous operations** - All protocols support async/await +- **Efficient data structures** - Fast lookups for agents, tasks, dependencies +- **Incremental cleanup** - Background processes for expired claims and inactive agents +- **Batched operations** - Support for bulk operations where appropriate + +## Git Integration + +The protocols are designed to work with git-based workflows: + +- **File-based persistence** - Uses JSON files that can be committed +- **Conflict-free merging** - Task modifications can be merged across branches +- **Incremental updates** - Only changed data is written to files +- **Defensive I/O** - Handles cloud sync issues that are common in git repos + +This protocol system provides a robust foundation for coordinated multi-agent task management while maintaining simplicity and reliability principles from the project's implementation philosophy. \ No newline at end of file diff --git a/amplifier/planner/protocols/__init__.py b/amplifier/planner/protocols/__init__.py new file mode 100644 index 00000000..b744fb65 --- /dev/null +++ b/amplifier/planner/protocols/__init__.py @@ -0,0 +1,79 @@ +""" +Task State Management and Coordination Protocols + +This package provides comprehensive protocols for managing task state transitions, +multi-agent coordination, deadlock prevention, conflict resolution, and defensive +programming patterns for the super-planner system. + +Key Components: +- State Transition Protocol: Manages valid task state changes with optimistic locking +- Agent Coordination Protocol: Handles task claiming, load balancing, and resource contention +- Deadlock Prevention Protocol: Detects and resolves circular dependencies and blocked chains +- Conflict Resolution Protocol: Manages concurrent modifications with merge strategies +- Defensive Coordination Utilities: Provides retry logic, health monitoring, and graceful degradation + +Usage Example: + from amplifier.planner.protocols import ( + state_protocol, + coordination_protocol, + deadlock_protocol, + conflict_protocol, + file_io, + health_monitor + ) + + # Transition a task state + updated_task = state_protocol.transition_task(task, TaskState.IN_PROGRESS) + + # Register an agent for coordination + await coordination_protocol.register_agent("agent_1", {"python", "ai"}) + + # Claim a task + claim = await coordination_protocol.claim_task("task_123", "agent_1") + + # Check for deadlocks + cycles = await deadlock_protocol.detect_deadlocks() + + # Resolve conflicts + resolved_task = await conflict_protocol.apply_modification_with_retry(task, modification) + + # Health monitoring + health_status = await health_monitor.full_health_check() +""" + +from .agent_coordination import AgentCoordinationProtocol +from .agent_coordination import coordination_protocol +from .conflict_resolution import ConflictResolutionProtocol +from .conflict_resolution import conflict_protocol +from .deadlock_prevention import DeadlockPreventionProtocol +from .deadlock_prevention import deadlock_protocol +from .defensive_coordination import CoordinationHealthCheck +from .defensive_coordination import DefensiveFileIO +from .defensive_coordination import GracefulDegradation +from .defensive_coordination import TaskOperationContext +from .defensive_coordination import file_io +from .defensive_coordination import health_monitor +from .defensive_coordination import retry_with_backoff +from .state_transitions import StateTransitionProtocol +from .state_transitions import state_protocol + +__all__ = [ + # Protocol classes + "StateTransitionProtocol", + "AgentCoordinationProtocol", + "DeadlockPreventionProtocol", + "ConflictResolutionProtocol", + "DefensiveFileIO", + "CoordinationHealthCheck", + "GracefulDegradation", + "TaskOperationContext", + # Global instances + "state_protocol", + "coordination_protocol", + "deadlock_protocol", + "conflict_protocol", + "file_io", + "health_monitor", + # Utilities + "retry_with_backoff", +] diff --git a/amplifier/planner/protocols/agent_coordination.py b/amplifier/planner/protocols/agent_coordination.py new file mode 100644 index 00000000..9cdfe2b1 --- /dev/null +++ b/amplifier/planner/protocols/agent_coordination.py @@ -0,0 +1,338 @@ +""" +Multi-Agent Coordination Protocol + +Manages task claiming, agent registration, load balancing, and resource contention +for the super-planner system. Ensures fair task distribution and prevents conflicts +between concurrent agents. +""" + +import asyncio +import logging +from dataclasses import dataclass +from dataclasses import field +from datetime import UTC +from datetime import datetime +from datetime import timedelta + +from ..core.models import Task + +logger = logging.getLogger(__name__) + + +class ClaimError(Exception): + """Raised when a task claim operation fails.""" + + def __init__(self, message: str = "Task claim operation failed") -> None: + super().__init__(message) + + +class LoadBalancingError(Exception): + """Raised when load balancing constraints are violated.""" + + def __init__(self, message: str = "Load balancing constraint violated") -> None: + super().__init__(message) + + +@dataclass +class AgentInfo: + """Information about a registered agent.""" + + agent_id: str + capabilities: set[str] = field(default_factory=set) + max_concurrent_tasks: int = 3 + current_task_count: int = 0 + last_heartbeat: datetime | None = None + is_active: bool = True + + +@dataclass +class TaskClaim: + """Represents a claimed task with lease information.""" + + task_id: str + agent_id: str + claimed_at: datetime + lease_expires_at: datetime + claim_version: int + + +class AgentCoordinationProtocol: + """ + Manages coordination between multiple agents working on tasks. + Handles claiming, load balancing, and resource contention. + """ + + def __init__(self, default_lease_duration: timedelta = timedelta(hours=1)): + self.agents: dict[str, AgentInfo] = {} + self.task_claims: dict[str, TaskClaim] = {} + self.default_lease_duration = default_lease_duration + self.heartbeat_timeout = timedelta(minutes=5) + self._lock = asyncio.Lock() + + async def register_agent(self, agent_id: str, capabilities: set[str], max_concurrent_tasks: int = 3) -> None: + """Register a new agent with the coordination system.""" + async with self._lock: + self.agents[agent_id] = AgentInfo( + agent_id=agent_id, + capabilities=capabilities, + max_concurrent_tasks=max_concurrent_tasks, + last_heartbeat=datetime.now(UTC), + is_active=True, + ) + logger.info(f"Agent {agent_id} registered with capabilities: {capabilities}") + + async def unregister_agent(self, agent_id: str) -> None: + """Unregister an agent and release its claimed tasks.""" + async with self._lock: + if agent_id not in self.agents: + return + + # Release all tasks claimed by this agent + tasks_to_release = [task_id for task_id, claim in self.task_claims.items() if claim.agent_id == agent_id] + + for task_id in tasks_to_release: + del self.task_claims[task_id] + logger.info(f"Released task {task_id} from unregistered agent {agent_id}") + + del self.agents[agent_id] + logger.info(f"Agent {agent_id} unregistered") + + async def heartbeat(self, agent_id: str) -> bool: + """Update agent heartbeat. Returns True if agent is still registered.""" + async with self._lock: + if agent_id in self.agents: + self.agents[agent_id].last_heartbeat = datetime.now(UTC) + self.agents[agent_id].is_active = True + return True + return False + + async def claim_task(self, task_id: str, agent_id: str, expected_task_version: int | None = None) -> TaskClaim: + """ + Attempt to claim a task for an agent. + + Args: + task_id: ID of task to claim + agent_id: ID of claiming agent + expected_task_version: Expected task version for optimistic locking + + Returns: + TaskClaim object if successful + + Raises: + ClaimError: If claim fails due to conflicts or constraints + """ + async with self._lock: + # Check if agent is registered and active + if agent_id not in self.agents: + raise ClaimError(f"Agent {agent_id} not registered") + + agent = self.agents[agent_id] + if not agent.is_active: + raise ClaimError(f"Agent {agent_id} is inactive") + + # Check if task is already claimed + if task_id in self.task_claims: + existing_claim = self.task_claims[task_id] + if existing_claim.lease_expires_at > datetime.now(UTC): + raise ClaimError(f"Task {task_id} already claimed by agent {existing_claim.agent_id}") + # Claim has expired, remove it + del self.task_claims[task_id] + + # Check agent's concurrent task limit + if agent.current_task_count >= agent.max_concurrent_tasks: + raise ClaimError(f"Agent {agent_id} at max concurrent tasks ({agent.max_concurrent_tasks})") + + # Create claim + claim = TaskClaim( + task_id=task_id, + agent_id=agent_id, + claimed_at=datetime.now(UTC), + lease_expires_at=datetime.now(UTC) + self.default_lease_duration, + claim_version=expected_task_version or 0, + ) + + # Store claim and update agent stats + self.task_claims[task_id] = claim + agent.current_task_count += 1 + + logger.info(f"Task {task_id} claimed by agent {agent_id}") + return claim + + async def release_task(self, task_id: str, agent_id: str) -> bool: + """ + Release a claimed task. + + Args: + task_id: ID of task to release + agent_id: ID of agent releasing the task + + Returns: + True if task was released, False if not claimed by this agent + """ + async with self._lock: + if task_id not in self.task_claims: + return False + + claim = self.task_claims[task_id] + if claim.agent_id != agent_id: + logger.warning(f"Agent {agent_id} tried to release task {task_id} claimed by {claim.agent_id}") + return False + + # Remove claim and update agent stats + del self.task_claims[task_id] + if agent_id in self.agents: + self.agents[agent_id].current_task_count = max(0, self.agents[agent_id].current_task_count - 1) + + logger.info(f"Task {task_id} released by agent {agent_id}") + return True + + async def renew_claim(self, task_id: str, agent_id: str) -> bool: + """ + Renew a task claim to extend the lease. + + Args: + task_id: ID of task to renew + agent_id: ID of agent renewing the claim + + Returns: + True if renewal successful, False otherwise + """ + async with self._lock: + if task_id not in self.task_claims: + return False + + claim = self.task_claims[task_id] + if claim.agent_id != agent_id: + return False + + # Extend the lease + claim.lease_expires_at = datetime.now(UTC) + self.default_lease_duration + logger.info(f"Task {task_id} claim renewed by agent {agent_id}") + return True + + def get_available_agents(self, required_capabilities: set[str] | None = None) -> list[AgentInfo]: + """ + Get list of agents available for new tasks. + + Args: + required_capabilities: Optional set of required capabilities + + Returns: + List of available agents, sorted by current load + """ + now = datetime.now(UTC) + available_agents = [] + + for agent in self.agents.values(): + # Check if agent is active and within heartbeat timeout + if not agent.is_active: + continue + + if agent.last_heartbeat and (now - agent.last_heartbeat) > self.heartbeat_timeout: + continue + + # Check if agent has capacity + if agent.current_task_count >= agent.max_concurrent_tasks: + continue + + # Check capabilities if required + if required_capabilities and not required_capabilities.issubset(agent.capabilities): + continue + + available_agents.append(agent) + + # Sort by current load (ascending) + available_agents.sort(key=lambda a: a.current_task_count) + return available_agents + + def select_best_agent(self, task: Task) -> str | None: + """ + Select the best agent for a given task using load balancing. + + Args: + task: Task to assign + + Returns: + Agent ID of best candidate, or None if no suitable agent available + """ + # Extract required capabilities from task metadata + required_capabilities = set() + if task.metadata and "required_capabilities" in task.metadata: + required_capabilities = set(task.metadata["required_capabilities"]) + + available_agents = self.get_available_agents(required_capabilities) + if not available_agents: + return None + + # Simple load balancing: pick agent with lowest current load + return available_agents[0].agent_id + + async def cleanup_expired_claims(self) -> int: + """ + Clean up expired task claims. + + Returns: + Number of claims cleaned up + """ + async with self._lock: + now = datetime.now(UTC) + expired_claims = [] + + for task_id, claim in self.task_claims.items(): + if claim.lease_expires_at <= now: + expired_claims.append(task_id) + + # Remove expired claims and update agent stats + for task_id in expired_claims: + claim = self.task_claims[task_id] + del self.task_claims[task_id] + + if claim.agent_id in self.agents: + self.agents[claim.agent_id].current_task_count = max( + 0, self.agents[claim.agent_id].current_task_count - 1 + ) + + logger.info(f"Expired claim for task {task_id} by agent {claim.agent_id}") + + return len(expired_claims) + + async def cleanup_inactive_agents(self) -> int: + """ + Clean up agents that haven't sent heartbeats recently. + + Returns: + Number of agents cleaned up + """ + now = datetime.now(UTC) + inactive_agents = [] + + for agent_id, agent in self.agents.items(): + if agent.last_heartbeat and (now - agent.last_heartbeat) > self.heartbeat_timeout: + inactive_agents.append(agent_id) + + for agent_id in inactive_agents: + await self.unregister_agent(agent_id) + + return len(inactive_agents) + + def get_coordination_stats(self) -> dict: + """Get current coordination system statistics.""" + now = datetime.now(UTC) + active_agents = sum(1 for a in self.agents.values() if a.is_active) + total_capacity = sum(a.max_concurrent_tasks for a in self.agents.values()) + used_capacity = sum(a.current_task_count for a in self.agents.values()) + active_claims = sum(1 for c in self.task_claims.values() if c.lease_expires_at > now) + + return { + "total_agents": len(self.agents), + "active_agents": active_agents, + "total_capacity": total_capacity, + "used_capacity": used_capacity, + "utilization_rate": used_capacity / max(total_capacity, 1), + "active_claims": active_claims, + "expired_claims": len(self.task_claims) - active_claims, + } + + +# Global coordination instance +coordination_protocol = AgentCoordinationProtocol() diff --git a/amplifier/planner/protocols/conflict_resolution.py b/amplifier/planner/protocols/conflict_resolution.py new file mode 100644 index 00000000..65f4b1a2 --- /dev/null +++ b/amplifier/planner/protocols/conflict_resolution.py @@ -0,0 +1,454 @@ +""" +Conflict Resolution and Consistency Protocol + +Handles concurrent task modifications, merge strategies, human escalation, +and maintains consistency across distributed operations in the task system. +""" + +import logging +from collections import defaultdict +from dataclasses import dataclass +from dataclasses import field +from datetime import UTC +from datetime import datetime +from datetime import timedelta +from enum import Enum +from typing import Any + +from ..core.models import Task + +logger = logging.getLogger(__name__) + + +class ConflictType(Enum): + """Types of conflicts that can occur.""" + + VERSION_MISMATCH = "version_mismatch" + CONCURRENT_STATE_CHANGE = "concurrent_state_change" + ASSIGNMENT_CONFLICT = "assignment_conflict" + DEPENDENCY_CONFLICT = "dependency_conflict" + METADATA_CONFLICT = "metadata_conflict" + + +class ResolutionStrategy(Enum): + """Conflict resolution strategies.""" + + LAST_WRITER_WINS = "last_writer_wins" + FIRST_WRITER_WINS = "first_writer_wins" + MERGE_CHANGES = "merge_changes" + ESCALATE_TO_HUMAN = "escalate_to_human" + REJECT_CONFLICT = "reject_conflict" + + +@dataclass +class ConflictRecord: + """Record of a conflict and its resolution.""" + + conflict_id: str + task_id: str + conflict_type: ConflictType + conflicting_versions: list[int] + conflicting_agents: list[str] + detected_at: datetime + resolution_strategy: ResolutionStrategy | None = None + resolved_at: datetime | None = None + resolution_details: dict[str, Any] = field(default_factory=dict) + escalated: bool = False + + +@dataclass +class TaskModification: + """Represents a modification to a task.""" + + task_id: str + agent_id: str + timestamp: datetime + previous_version: int + changes: dict[str, Any] + new_task_state: Task + + +class ConflictResolutionProtocol: + """ + Manages conflict detection and resolution for concurrent task modifications. + Provides multiple resolution strategies and escalation paths. + """ + + def __init__(self, auto_retry_attempts: int = 3, escalation_timeout: timedelta = timedelta(minutes=30)): + self.conflicts: dict[str, ConflictRecord] = {} + self.pending_modifications: dict[str, list[TaskModification]] = defaultdict(list) + self.resolution_strategies: dict[ConflictType, ResolutionStrategy] = { + ConflictType.VERSION_MISMATCH: ResolutionStrategy.MERGE_CHANGES, + ConflictType.CONCURRENT_STATE_CHANGE: ResolutionStrategy.LAST_WRITER_WINS, + ConflictType.ASSIGNMENT_CONFLICT: ResolutionStrategy.ESCALATE_TO_HUMAN, + ConflictType.DEPENDENCY_CONFLICT: ResolutionStrategy.MERGE_CHANGES, + ConflictType.METADATA_CONFLICT: ResolutionStrategy.MERGE_CHANGES, + } + self.auto_retry_attempts = auto_retry_attempts + self.escalation_timeout = escalation_timeout + + def detect_conflict(self, current_task: Task, modification: TaskModification) -> ConflictType | None: + """ + Detect if a modification conflicts with the current task state. + + Args: + current_task: Current task state + modification: Proposed modification + + Returns: + ConflictType if conflict detected, None otherwise + """ + # Version mismatch + if current_task.version != modification.previous_version: + return ConflictType.VERSION_MISMATCH + + # Concurrent state changes + proposed_task = modification.new_task_state + if current_task.state != proposed_task.state and current_task.updated_at > modification.timestamp: + return ConflictType.CONCURRENT_STATE_CHANGE + + # Assignment conflicts + if ( + current_task.assigned_to != proposed_task.assigned_to + and current_task.assigned_to is not None + and proposed_task.assigned_to is not None + and current_task.assigned_to != proposed_task.assigned_to + ): + return ConflictType.ASSIGNMENT_CONFLICT + + # Dependency conflicts (simplified check) + current_deps = set(current_task.dependencies or []) + proposed_deps = set(proposed_task.dependencies or []) + if current_deps != proposed_deps and len(current_deps) > 0 and len(proposed_deps) > 0: + return ConflictType.DEPENDENCY_CONFLICT + + return None + + async def resolve_conflict( + self, conflict_record: ConflictRecord, current_task: Task, conflicting_modifications: list[TaskModification] + ) -> Task: + """ + Resolve a conflict using the appropriate strategy. + + Args: + conflict_record: Record of the conflict + current_task: Current task state + conflicting_modifications: List of conflicting modifications + + Returns: + Resolved task state + + Raises: + ValueError: If conflict cannot be resolved automatically + """ + strategy = self.resolution_strategies.get(conflict_record.conflict_type, ResolutionStrategy.ESCALATE_TO_HUMAN) + + conflict_record.resolution_strategy = strategy + + if strategy == ResolutionStrategy.LAST_WRITER_WINS: + return await self._resolve_last_writer_wins(current_task, conflicting_modifications) + + if strategy == ResolutionStrategy.FIRST_WRITER_WINS: + return await self._resolve_first_writer_wins(current_task, conflicting_modifications) + + if strategy == ResolutionStrategy.MERGE_CHANGES: + return await self._resolve_merge_changes(current_task, conflicting_modifications) + + if strategy == ResolutionStrategy.ESCALATE_TO_HUMAN: + await self._escalate_conflict(conflict_record, current_task, conflicting_modifications) + raise ValueError(f"Conflict {conflict_record.conflict_id} escalated to human intervention") + + if strategy == ResolutionStrategy.REJECT_CONFLICT: + raise ValueError(f"Conflict {conflict_record.conflict_id} rejected") + + raise ValueError(f"Unknown resolution strategy: {strategy}") + + async def _resolve_last_writer_wins(self, current_task: Task, modifications: list[TaskModification]) -> Task: + """Resolve conflict by using the most recent modification.""" + latest_mod = max(modifications, key=lambda m: m.timestamp) + + # Apply the latest modification with version increment + resolved_task = Task( + id=current_task.id, + title=latest_mod.new_task_state.title, + description=latest_mod.new_task_state.description, + state=latest_mod.new_task_state.state, + priority=latest_mod.new_task_state.priority, + estimated_effort=latest_mod.new_task_state.estimated_effort, + assigned_to=latest_mod.new_task_state.assigned_to, + dependencies=latest_mod.new_task_state.dependencies, + metadata=latest_mod.new_task_state.metadata, + parent_id=latest_mod.new_task_state.parent_id, + subtask_ids=latest_mod.new_task_state.subtask_ids, + created_at=current_task.created_at, + updated_at=datetime.now(UTC), + started_at=latest_mod.new_task_state.started_at, + completed_at=latest_mod.new_task_state.completed_at, + cancelled_at=latest_mod.new_task_state.cancelled_at, + blocked_at=latest_mod.new_task_state.blocked_at, + blocking_reason=latest_mod.new_task_state.blocking_reason, + version=current_task.version + 1, + ) + + logger.info(f"Resolved conflict using last writer wins: agent {latest_mod.agent_id}") + return resolved_task + + async def _resolve_first_writer_wins(self, current_task: Task, modifications: list[TaskModification]) -> Task: + """Resolve conflict by using the earliest modification.""" + earliest_mod = min(modifications, key=lambda m: m.timestamp) + + # Apply the earliest modification with version increment + resolved_task = Task( + id=current_task.id, + title=earliest_mod.new_task_state.title, + description=earliest_mod.new_task_state.description, + state=earliest_mod.new_task_state.state, + priority=earliest_mod.new_task_state.priority, + estimated_effort=earliest_mod.new_task_state.estimated_effort, + assigned_to=earliest_mod.new_task_state.assigned_to, + dependencies=earliest_mod.new_task_state.dependencies, + metadata=earliest_mod.new_task_state.metadata, + parent_id=earliest_mod.new_task_state.parent_id, + subtask_ids=earliest_mod.new_task_state.subtask_ids, + created_at=current_task.created_at, + updated_at=datetime.now(UTC), + started_at=earliest_mod.new_task_state.started_at, + completed_at=earliest_mod.new_task_state.completed_at, + cancelled_at=earliest_mod.new_task_state.cancelled_at, + blocked_at=earliest_mod.new_task_state.blocked_at, + blocking_reason=earliest_mod.new_task_state.blocking_reason, + version=current_task.version + 1, + ) + + logger.info(f"Resolved conflict using first writer wins: agent {earliest_mod.agent_id}") + return resolved_task + + async def _resolve_merge_changes(self, current_task: Task, modifications: list[TaskModification]) -> Task: + """ + Resolve conflict by intelligently merging non-conflicting changes. + """ + # Start with current task as base + merged_task = current_task + + # Sort modifications by timestamp + sorted_mods = sorted(modifications, key=lambda m: m.timestamp) + + for mod in sorted_mods: + merged_task = await self._merge_single_modification(merged_task, mod) + + # Update version and timestamp + merged_task = Task( + id=merged_task.id, + title=merged_task.title, + description=merged_task.description, + state=merged_task.state, + priority=merged_task.priority, + estimated_effort=merged_task.estimated_effort, + assigned_to=merged_task.assigned_to, + dependencies=merged_task.dependencies, + metadata=merged_task.metadata, + parent_id=merged_task.parent_id, + subtask_ids=merged_task.subtask_ids, + created_at=merged_task.created_at, + updated_at=datetime.now(UTC), + started_at=merged_task.started_at, + completed_at=merged_task.completed_at, + cancelled_at=merged_task.cancelled_at, + blocked_at=merged_task.blocked_at, + blocking_reason=merged_task.blocking_reason, + version=current_task.version + 1, + ) + + logger.info(f"Resolved conflict by merging changes from {len(modifications)} modifications") + return merged_task + + async def _merge_single_modification(self, base_task: Task, modification: TaskModification) -> Task: + """Merge a single modification into the base task.""" + new_state = modification.new_task_state + + # Merge metadata (additive) + merged_metadata = (base_task.metadata or {}).copy() + if new_state.metadata: + merged_metadata.update(new_state.metadata) + + # Merge dependencies (union) + base_deps = set(base_task.dependencies or []) + new_deps = set(new_state.dependencies or []) + merged_deps = list(base_deps.union(new_deps)) + + # Merge subtasks (union) + base_subtasks = set(base_task.subtask_ids or []) + new_subtasks = set(new_state.subtask_ids or []) + merged_subtasks = list(base_subtasks.union(new_subtasks)) + + # For other fields, prefer non-null values from modification + return Task( + id=base_task.id, + title=new_state.title if new_state.title != base_task.title else base_task.title, + description=new_state.description + if new_state.description != base_task.description + else base_task.description, + state=new_state.state if new_state.state != base_task.state else base_task.state, + priority=new_state.priority if new_state.priority != base_task.priority else base_task.priority, + estimated_effort=new_state.estimated_effort + if new_state.estimated_effort != base_task.estimated_effort + else base_task.estimated_effort, + assigned_to=new_state.assigned_to or base_task.assigned_to, + dependencies=merged_deps, + metadata=merged_metadata, + parent_id=new_state.parent_id or base_task.parent_id, + subtask_ids=merged_subtasks, + created_at=base_task.created_at, + updated_at=base_task.updated_at, + started_at=new_state.started_at or base_task.started_at, + completed_at=new_state.completed_at or base_task.completed_at, + cancelled_at=new_state.cancelled_at or base_task.cancelled_at, + blocked_at=new_state.blocked_at or base_task.blocked_at, + blocking_reason=new_state.blocking_reason or base_task.blocking_reason, + version=base_task.version, + ) + + async def _escalate_conflict( + self, conflict_record: ConflictRecord, current_task: Task, modifications: list[TaskModification] + ) -> None: + """Escalate conflict to human intervention.""" + conflict_record.escalated = True + + # Log detailed conflict information + logger.critical(f"CONFLICT ESCALATION: {conflict_record.conflict_id}") + logger.critical(f"Task ID: {current_task.id}") + logger.critical(f"Conflict Type: {conflict_record.conflict_type}") + logger.critical(f"Conflicting Agents: {conflict_record.conflicting_agents}") + logger.critical(f"Versions: {conflict_record.conflicting_versions}") + + # Store detailed information for human review + conflict_record.resolution_details = { + "current_task": self._task_to_dict(current_task), + "modifications": [ + { + "agent_id": mod.agent_id, + "timestamp": mod.timestamp.isoformat(), + "changes": mod.changes, + "new_state": self._task_to_dict(mod.new_task_state), + } + for mod in modifications + ], + "escalated_at": datetime.now(UTC).isoformat(), + } + + def _task_to_dict(self, task: Task) -> dict[str, Any]: + """Convert task to dictionary for logging/storage.""" + return { + "id": task.id, + "title": task.title, + "description": task.description, + "state": task.state.value, + "priority": task.priority, + "estimated_effort": task.estimated_effort, + "assigned_to": task.assigned_to, + "dependencies": task.dependencies, + "metadata": task.metadata, + "parent_id": task.parent_id, + "subtask_ids": task.subtask_ids, + "version": task.version, + "created_at": task.created_at.isoformat() if task.created_at else None, + "updated_at": task.updated_at.isoformat() if task.updated_at else None, + "started_at": task.started_at.isoformat() if task.started_at else None, + "completed_at": task.completed_at.isoformat() if task.completed_at else None, + "cancelled_at": task.cancelled_at.isoformat() if task.cancelled_at else None, + "blocked_at": task.blocked_at.isoformat() if task.blocked_at else None, + "blocking_reason": task.blocking_reason, + } + + async def apply_modification_with_retry(self, current_task: Task, modification: TaskModification) -> Task: + """ + Apply a modification with automatic conflict resolution and retry. + + Args: + current_task: Current task state + modification: Modification to apply + + Returns: + Updated task after resolution + + Raises: + ValueError: If modification cannot be applied after retries + """ + for attempt in range(self.auto_retry_attempts): + conflict_type = self.detect_conflict(current_task, modification) + + if conflict_type is None: + # No conflict, apply modification directly + return modification.new_task_state + + # Create conflict record + conflict_id = f"{modification.task_id}_{modification.timestamp.timestamp()}" + conflict_record = ConflictRecord( + conflict_id=conflict_id, + task_id=modification.task_id, + conflict_type=conflict_type, + conflicting_versions=[current_task.version, modification.previous_version], + conflicting_agents=[modification.agent_id], + detected_at=datetime.now(UTC), + ) + + self.conflicts[conflict_id] = conflict_record + + try: + # Attempt to resolve conflict + resolved_task = await self.resolve_conflict(conflict_record, current_task, [modification]) + + conflict_record.resolved_at = datetime.now(UTC) + logger.info(f"Resolved conflict {conflict_id} on attempt {attempt + 1}") + + return resolved_task + + except ValueError as e: + if "escalated" in str(e): + # Human intervention required + raise e + + if attempt == self.auto_retry_attempts - 1: + # Final attempt failed + logger.error(f"Failed to resolve conflict {conflict_id} after {self.auto_retry_attempts} attempts") + raise e + + # Retry with exponential backoff + import asyncio + + await asyncio.sleep(2**attempt) + logger.warning(f"Retrying conflict resolution for {conflict_id}, attempt {attempt + 2}") + + raise ValueError(f"Could not resolve conflict after {self.auto_retry_attempts} attempts") + + def get_escalated_conflicts(self) -> list[ConflictRecord]: + """Get all conflicts that require human intervention.""" + return [conflict for conflict in self.conflicts.values() if conflict.escalated and conflict.resolved_at is None] + + def get_conflict_stats(self) -> dict[str, Any]: + """Get conflict resolution statistics.""" + total_conflicts = len(self.conflicts) + resolved_conflicts = sum(1 for c in self.conflicts.values() if c.resolved_at is not None) + escalated_conflicts = sum(1 for c in self.conflicts.values() if c.escalated) + + conflict_types = defaultdict(int) + for conflict in self.conflicts.values(): + conflict_types[conflict.conflict_type.value] += 1 + + resolution_strategies = defaultdict(int) + for conflict in self.conflicts.values(): + if conflict.resolution_strategy: + resolution_strategies[conflict.resolution_strategy.value] += 1 + + return { + "total_conflicts": total_conflicts, + "resolved_conflicts": resolved_conflicts, + "escalated_conflicts": escalated_conflicts, + "resolution_rate": resolved_conflicts / max(total_conflicts, 1), + "conflict_types": dict(conflict_types), + "resolution_strategies": dict(resolution_strategies), + } + + +# Global conflict resolution instance +conflict_protocol = ConflictResolutionProtocol() diff --git a/amplifier/planner/protocols/deadlock_prevention.py b/amplifier/planner/protocols/deadlock_prevention.py new file mode 100644 index 00000000..372382db --- /dev/null +++ b/amplifier/planner/protocols/deadlock_prevention.py @@ -0,0 +1,399 @@ +""" +Deadlock Prevention and Recovery Protocol + +Detects circular dependencies, handles blocked task chains, implements timeout policies, +and provides escalation procedures to prevent and resolve deadlocks in the task system. +""" + +import asyncio +import logging +from collections import defaultdict +from dataclasses import dataclass +from datetime import UTC +from datetime import datetime +from datetime import timedelta + +logger = logging.getLogger(__name__) + + +class DeadlockError(Exception): + """Raised when a deadlock is detected.""" + + def __init__(self, message: str = "Deadlock detected in task execution") -> None: + super().__init__(message) + + +class CircularDependencyError(Exception): + """Raised when circular dependencies are detected.""" + + def __init__(self, message: str = "Circular dependency detected") -> None: + super().__init__(message) + + +@dataclass +class BlockedTaskInfo: + """Information about a blocked task.""" + + task_id: str + blocked_at: datetime + blocking_reason: str + dependencies: list[str] + escalated: bool = False + + +@dataclass +class DeadlockCycle: + """Represents a detected deadlock cycle.""" + + task_ids: list[str] + cycle_length: int + detected_at: datetime + severity: str = "medium" # low, medium, high, critical + + +class DeadlockPreventionProtocol: + """ + Prevents and resolves deadlocks in the task dependency system. + Uses cycle detection, timeouts, and escalation to maintain system health. + """ + + def __init__( + self, + max_block_duration: timedelta = timedelta(hours=4), + escalation_threshold: timedelta = timedelta(hours=1), + max_dependency_depth: int = 10, + ): + self.blocked_tasks: dict[str, BlockedTaskInfo] = {} + self.dependency_graph: dict[str, set[str]] = defaultdict(set) + self.reverse_graph: dict[str, set[str]] = defaultdict(set) + self.max_block_duration = max_block_duration + self.escalation_threshold = escalation_threshold + self.max_dependency_depth = max_dependency_depth + self._lock = asyncio.Lock() + + async def add_dependency(self, dependent_task_id: str, dependency_task_id: str) -> bool: + """ + Add a dependency between tasks, checking for cycles. + + Args: + dependent_task_id: Task that depends on another + dependency_task_id: Task that is depended upon + + Returns: + True if dependency added successfully, False if it would create a cycle + + Raises: + CircularDependencyError: If adding the dependency would create a cycle + """ + async with self._lock: + # Check if adding this dependency would create a cycle + if self._would_create_cycle(dependent_task_id, dependency_task_id): + raise CircularDependencyError( + f"Adding dependency {dependent_task_id} -> {dependency_task_id} would create a circular dependency" + ) + + # Check dependency depth + depth = self._calculate_dependency_depth(dependency_task_id) + if depth >= self.max_dependency_depth: + logger.warning( + f"Dependency chain too deep ({depth}) for {dependency_task_id}, " + f"maximum is {self.max_dependency_depth}" + ) + return False + + # Add dependency + self.dependency_graph[dependent_task_id].add(dependency_task_id) + self.reverse_graph[dependency_task_id].add(dependent_task_id) + + logger.info(f"Added dependency: {dependent_task_id} depends on {dependency_task_id}") + return True + + async def remove_dependency(self, dependent_task_id: str, dependency_task_id: str) -> bool: + """Remove a dependency between tasks.""" + async with self._lock: + if dependency_task_id in self.dependency_graph[dependent_task_id]: + self.dependency_graph[dependent_task_id].discard(dependency_task_id) + self.reverse_graph[dependency_task_id].discard(dependent_task_id) + + # Clean up empty entries + if not self.dependency_graph[dependent_task_id]: + del self.dependency_graph[dependent_task_id] + if not self.reverse_graph[dependency_task_id]: + del self.reverse_graph[dependency_task_id] + + logger.info(f"Removed dependency: {dependent_task_id} no longer depends on {dependency_task_id}") + return True + return False + + async def mark_task_blocked( + self, task_id: str, blocking_reason: str, dependencies: list[str] | None = None + ) -> None: + """Mark a task as blocked and track it for deadlock detection.""" + async with self._lock: + self.blocked_tasks[task_id] = BlockedTaskInfo( + task_id=task_id, + blocked_at=datetime.now(UTC), + blocking_reason=blocking_reason, + dependencies=dependencies or list(self.dependency_graph.get(task_id, [])), + ) + logger.info(f"Task {task_id} marked as blocked: {blocking_reason}") + + async def mark_task_unblocked(self, task_id: str) -> bool: + """Mark a task as unblocked and remove from tracking.""" + async with self._lock: + if task_id in self.blocked_tasks: + del self.blocked_tasks[task_id] + logger.info(f"Task {task_id} marked as unblocked") + return True + return False + + def _would_create_cycle(self, from_task: str, to_task: str) -> bool: + """Check if adding a dependency would create a cycle using DFS.""" + # If to_task can reach from_task, then adding from_task -> to_task creates a cycle + return self._can_reach(to_task, from_task) + + def _can_reach(self, start_task: str, target_task: str) -> bool: + """Check if start_task can reach target_task through dependencies.""" + if start_task == target_task: + return True + + visited = set() + stack = [start_task] + + while stack: + current = stack.pop() + if current == target_task: + return True + + if current in visited: + continue + + visited.add(current) + stack.extend(self.dependency_graph.get(current, [])) + + return False + + def _calculate_dependency_depth(self, task_id: str) -> int: + """Calculate the maximum depth of the dependency chain starting from task_id.""" + visited = set() + + def dfs_depth(current_task: str) -> int: + if current_task in visited: + return 0 # Avoid infinite recursion + + visited.add(current_task) + max_depth = 0 + + for dependency in self.dependency_graph.get(current_task, []): + depth = 1 + dfs_depth(dependency) + max_depth = max(max_depth, depth) + + visited.remove(current_task) + return max_depth + + return dfs_depth(task_id) + + async def detect_deadlocks(self) -> list[DeadlockCycle]: + """ + Detect all deadlock cycles in the current task system. + + Returns: + List of detected deadlock cycles + """ + async with self._lock: + cycles = [] + visited_global = set() + + # Check each unvisited node for cycles + for task_id in self.dependency_graph: + if task_id not in visited_global: + cycle = self._detect_cycle_from_node(task_id, visited_global) + if cycle: + cycles.append( + DeadlockCycle( + task_ids=cycle, + cycle_length=len(cycle), + detected_at=datetime.now(UTC), + severity=self._assess_cycle_severity(cycle), + ) + ) + + if cycles: + logger.warning(f"Detected {len(cycles)} deadlock cycles") + + return cycles + + def _detect_cycle_from_node(self, start_node: str, visited_global: set) -> list[str] | None: + """Detect cycle starting from a specific node using DFS.""" + stack = [] + visited_local = set() + + def dfs(node: str) -> list[str] | None: + if node in visited_local: + # Found a cycle, extract it + cycle_start = stack.index(node) + return stack[cycle_start:] + [node] + + if node in visited_global: + return None + + visited_local.add(node) + visited_global.add(node) + stack.append(node) + + for neighbor in self.dependency_graph.get(node, []): + result = dfs(neighbor) + if result: + return result + + stack.pop() + return None + + return dfs(start_node) + + def _assess_cycle_severity(self, cycle: list[str]) -> str: + """Assess the severity of a deadlock cycle based on various factors.""" + cycle_length = len(cycle) + blocked_tasks_in_cycle = sum(1 for task_id in cycle if task_id in self.blocked_tasks) + + if blocked_tasks_in_cycle >= len(cycle): + return "critical" # All tasks in cycle are blocked + if cycle_length <= 2: + return "high" # Simple direct cycle + if blocked_tasks_in_cycle > len(cycle) // 2: + return "medium" # More than half the cycle is blocked + return "low" # Potential future deadlock + + async def resolve_deadlock(self, cycle: DeadlockCycle) -> bool: + """ + Attempt to resolve a deadlock by breaking the cycle. + + Args: + cycle: Deadlock cycle to resolve + + Returns: + True if deadlock was resolved, False if manual intervention needed + """ + async with self._lock: + logger.warning(f"Attempting to resolve deadlock cycle: {cycle.task_ids}") + + # Strategy 1: Find the weakest link to break + weakest_dependency = self._find_weakest_dependency(cycle.task_ids) + if weakest_dependency: + from_task, to_task = weakest_dependency + await self.remove_dependency(from_task, to_task) + logger.info(f"Broke deadlock by removing dependency: {from_task} -> {to_task}") + return True + + # Strategy 2: Cancel the most recently blocked task in the cycle + most_recent_blocked = self._find_most_recent_blocked_task(cycle.task_ids) + if most_recent_blocked: + # This would need to be handled by the task manager + logger.info(f"Recommended canceling task {most_recent_blocked} to break deadlock") + return True + + # Strategy 3: Escalate to human intervention + logger.error(f"Could not automatically resolve deadlock cycle: {cycle.task_ids}") + await self._escalate_deadlock(cycle) + return False + + def _find_weakest_dependency(self, cycle_tasks: list[str]) -> tuple[str, str] | None: + """Find the weakest dependency link in the cycle to break.""" + # Look for dependencies where the target task is not blocked + # or has the least number of dependents + min_dependents = float("inf") + weakest_link = None + + for i in range(len(cycle_tasks)): + from_task = cycle_tasks[i] + to_task = cycle_tasks[(i + 1) % len(cycle_tasks)] + + if to_task in self.dependency_graph[from_task]: + # Count how many tasks depend on to_task + dependent_count = len(self.reverse_graph.get(to_task, [])) + + # Prefer non-blocked tasks + if to_task not in self.blocked_tasks: + dependent_count -= 100 # Strong preference + + if dependent_count < min_dependents: + min_dependents = dependent_count + weakest_link = (from_task, to_task) + + return weakest_link + + def _find_most_recent_blocked_task(self, cycle_tasks: list[str]) -> str | None: + """Find the most recently blocked task in the cycle.""" + most_recent_task = None + most_recent_time = None + + for task_id in cycle_tasks: + if task_id in self.blocked_tasks: + blocked_info = self.blocked_tasks[task_id] + if most_recent_time is None or blocked_info.blocked_at > most_recent_time: + most_recent_time = blocked_info.blocked_at + most_recent_task = task_id + + return most_recent_task + + async def _escalate_deadlock(self, cycle: DeadlockCycle) -> None: + """Escalate deadlock to human intervention.""" + # Mark all tasks in cycle as escalated + for task_id in cycle.task_ids: + if task_id in self.blocked_tasks: + self.blocked_tasks[task_id].escalated = True + + # Log detailed information for human review + logger.critical(f"DEADLOCK ESCALATION REQUIRED: Cycle {cycle.task_ids}") + logger.critical(f"Cycle severity: {cycle.severity}") + logger.critical(f"Cycle detected at: {cycle.detected_at}") + + # In a real system, this would trigger alerts, notifications, etc. + + async def check_timeout_violations(self) -> list[str]: + """ + Check for tasks that have been blocked too long and need intervention. + + Returns: + List of task IDs that have exceeded timeout thresholds + """ + now = datetime.now(UTC) + violations = [] + + for task_id, blocked_info in self.blocked_tasks.items(): + time_blocked = now - blocked_info.blocked_at + + if time_blocked > self.max_block_duration: + violations.append(task_id) + logger.error(f"Task {task_id} blocked for {time_blocked}, exceeds maximum {self.max_block_duration}") + elif time_blocked > self.escalation_threshold and not blocked_info.escalated: + # Mark for escalation + blocked_info.escalated = True + logger.warning(f"Task {task_id} blocked for {time_blocked}, escalating") + + return violations + + async def get_deadlock_stats(self) -> dict: + """Get current deadlock prevention system statistics.""" + cycles = await self.detect_deadlocks() + now = datetime.now(UTC) + + escalated_count = sum(1 for info in self.blocked_tasks.values() if info.escalated) + overdue_count = sum( + 1 for info in self.blocked_tasks.values() if (now - info.blocked_at) > self.max_block_duration + ) + + return { + "total_dependencies": sum(len(deps) for deps in self.dependency_graph.values()), + "blocked_tasks": len(self.blocked_tasks), + "active_deadlocks": len(cycles), + "escalated_tasks": escalated_count, + "overdue_tasks": overdue_count, + "max_dependency_depth": max( + (self._calculate_dependency_depth(task_id) for task_id in self.dependency_graph), default=0 + ), + } + + +# Global deadlock prevention instance +deadlock_protocol = DeadlockPreventionProtocol() diff --git a/amplifier/planner/protocols/defensive_coordination.py b/amplifier/planner/protocols/defensive_coordination.py new file mode 100644 index 00000000..96d962fc --- /dev/null +++ b/amplifier/planner/protocols/defensive_coordination.py @@ -0,0 +1,402 @@ +""" +Defensive Coordination Utilities + +Provides defensive programming utilities for the super-planner coordination system, +following patterns from ccsdk_toolkit with retry logic, error recovery, and graceful +degradation strategies. +""" + +import asyncio +import json +import logging +from collections.abc import Callable +from dataclasses import dataclass +from datetime import UTC +from datetime import datetime +from datetime import timedelta +from functools import wraps +from pathlib import Path +from typing import Any +from typing import TypeVar + +from ..core.models import Task +from .state_transitions import state_protocol + +logger = logging.getLogger(__name__) + +T = TypeVar("T") + + +@dataclass +class RetryConfig: + """Configuration for retry operations.""" + + max_attempts: int = 3 + initial_delay: float = 1.0 + backoff_multiplier: float = 2.0 + max_delay: float = 60.0 + retry_on_exceptions: tuple = (ConnectionError, TimeoutError, OSError) + + +@dataclass +class CircuitBreakerConfig: + """Configuration for circuit breaker pattern.""" + + failure_threshold: int = 5 + recovery_timeout: timedelta = timedelta(minutes=5) + half_open_max_calls: int = 3 + + +class CircuitBreakerState: + """Circuit breaker state management.""" + + def __init__(self, config: CircuitBreakerConfig): + self.config = config + self.failure_count = 0 + self.last_failure_time: datetime | None = None + self.state = "closed" # closed, open, half_open + self.half_open_calls = 0 + + +def retry_with_backoff(config: RetryConfig | None = None): + """ + Decorator for retry with exponential backoff. + Based on defensive utilities patterns from DISCOVERIES.md. + """ + if config is None: + config = RetryConfig() + + def decorator(func: Callable[..., T]) -> Callable[..., T]: + @wraps(func) + async def async_wrapper(*args, **kwargs) -> T: + last_exception: Exception | None = None + delay = config.initial_delay + + for attempt in range(config.max_attempts): + try: + if asyncio.iscoroutinefunction(func): + return await func(*args, **kwargs) + return func(*args, **kwargs) # type: ignore + + except config.retry_on_exceptions as e: + last_exception = e + + if attempt == 0: + logger.warning( + f"Operation failed ({func.__name__}), retrying. " + f"This may be due to network issues or temporary failures. " + f"Error: {e}" + ) + + if attempt < config.max_attempts - 1: + await asyncio.sleep(min(delay, config.max_delay)) + delay *= config.backoff_multiplier + else: + logger.error(f"Operation failed after {config.max_attempts} attempts: {e}") + raise e + + except Exception as e: + # Don't retry on non-recoverable exceptions + logger.error(f"Non-recoverable error in {func.__name__}: {e}") + raise e + + if last_exception: + raise last_exception + raise RuntimeError("Unexpected retry loop exit") + + @wraps(func) + def sync_wrapper(*args, **kwargs) -> T: + return asyncio.run(async_wrapper(*args, **kwargs)) + + if asyncio.iscoroutinefunction(func): + return async_wrapper # type: ignore + return sync_wrapper + + return decorator + + +class DefensiveFileIO: + """ + Defensive file I/O operations with retry logic and cloud sync handling. + Based on OneDrive/Cloud Sync patterns from DISCOVERIES.md. + """ + + def __init__(self, max_retries: int = 5, initial_delay: float = 0.5): + self.max_retries = max_retries + self.initial_delay = initial_delay + + @retry_with_backoff( + RetryConfig( + max_attempts=5, + initial_delay=0.5, + backoff_multiplier=2.0, + retry_on_exceptions=(OSError, IOError, PermissionError), + ) + ) + def write_json(self, data: Any, filepath: Path, ensure_newline: bool = True) -> None: + """Write JSON data to file with defensive error handling.""" + try: + # Ensure parent directory exists + filepath.parent.mkdir(parents=True, exist_ok=True) + + # Write with explicit encoding and flushing + with open(filepath, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + if ensure_newline: + f.write("\n") + f.flush() + + except OSError as e: + if e.errno == 5: # I/O error - likely cloud sync issue + logger.warning( + f"File I/O error writing to {filepath} - likely cloud sync delay. " + f"Consider enabling 'Always keep on this device' for: {filepath.parent}" + ) + raise + + @retry_with_backoff(RetryConfig(max_attempts=3, retry_on_exceptions=(OSError, IOError, json.JSONDecodeError))) + def read_json(self, filepath: Path) -> Any: + """Read JSON data from file with defensive error handling.""" + try: + with open(filepath, encoding="utf-8") as f: + return json.load(f) + except FileNotFoundError: + logger.info(f"File not found: {filepath}") + return None + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in {filepath}: {e}") + raise + + @retry_with_backoff(RetryConfig(max_attempts=3, retry_on_exceptions=(OSError, IOError))) + def append_jsonl(self, data: Any, filepath: Path) -> None: + """Append JSON line to file with defensive error handling.""" + try: + filepath.parent.mkdir(parents=True, exist_ok=True) + + with open(filepath, "a", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False) + f.write("\n") + f.flush() + + except OSError as e: + if e.errno == 5: + logger.warning(f"File I/O error appending to {filepath} - likely cloud sync delay.") + raise + + +class TaskOperationContext: + """ + Context manager for safe task operations with automatic rollback. + """ + + def __init__(self, task: Task, operation_name: str): + self.original_task = task + self.operation_name = operation_name + self.current_task = task + self.completed_successfully = False + + async def __aenter__(self): + logger.debug(f"Starting {self.operation_name} for task {self.original_task.id}") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self.completed_successfully = True + logger.debug(f"Completed {self.operation_name} for task {self.original_task.id}") + else: + logger.warning(f"Failed {self.operation_name} for task {self.original_task.id}: {exc_val}") + # In a real system, this might trigger rollback operations + return False + + def update_task(self, new_task: Task) -> None: + """Update the current task state.""" + self.current_task = new_task + + +class CoordinationHealthCheck: + """ + Health monitoring for coordination system components. + """ + + def __init__(self): + self.component_status: dict[str, dict[str, Any]] = {} + self.last_check_time: dict[str, datetime] = {} + + async def check_state_transitions(self) -> dict[str, Any]: + """Check state transition system health.""" + try: + # Test basic state transition validation + from ..core.models import Task + from ..core.models import TaskState + + test_task = Task( + id="health_check", + title="Health Check Task", + description="Test task for health checking", + state=TaskState.NOT_STARTED, + version=1, + ) + + # Test transition validation + can_transition, reason = state_protocol.can_transition(test_task, TaskState.ASSIGNED) + + status = { + "status": "healthy" if not can_transition else "healthy", + "last_check": datetime.now(UTC).isoformat(), + "transition_validation": "working", + "details": f"Can transition check: {can_transition}, reason: {reason}", + } + + except Exception as e: + status = {"status": "unhealthy", "last_check": datetime.now(UTC).isoformat(), "error": str(e)} + + self.component_status["state_transitions"] = status + self.last_check_time["state_transitions"] = datetime.now(UTC) + return status + + async def check_coordination_protocol(self) -> dict[str, Any]: + """Check agent coordination protocol health.""" + try: + from .agent_coordination import coordination_protocol + + stats = coordination_protocol.get_coordination_stats() + + status = { + "status": "healthy", + "last_check": datetime.now(UTC).isoformat(), + "stats": stats, + "active_agents": stats.get("active_agents", 0), + "utilization_rate": stats.get("utilization_rate", 0.0), + } + + # Check for concerning patterns + if stats.get("utilization_rate", 0) > 0.9: + status["warnings"] = ["High utilization rate"] + + if stats.get("expired_claims", 0) > 10: + status["warnings"] = status.get("warnings", []) + ["Many expired claims"] + + except Exception as e: + status = {"status": "unhealthy", "last_check": datetime.now(UTC).isoformat(), "error": str(e)} + + self.component_status["coordination"] = status + self.last_check_time["coordination"] = datetime.now(UTC) + return status + + async def check_deadlock_prevention(self) -> dict[str, Any]: + """Check deadlock prevention system health.""" + try: + from .deadlock_prevention import deadlock_protocol + + stats = await deadlock_protocol.get_deadlock_stats() + + status = { + "status": "healthy", + "last_check": datetime.now(UTC).isoformat(), + "stats": stats, + "active_deadlocks": stats.get("active_deadlocks", 0), + "blocked_tasks": stats.get("blocked_tasks", 0), + } + + # Check for concerning patterns + if stats.get("active_deadlocks", 0) > 0: + status["status"] = "warning" + status["warnings"] = ["Active deadlocks detected"] + + if stats.get("overdue_tasks", 0) > 0: + status["warnings"] = status.get("warnings", []) + ["Tasks overdue for resolution"] + + except Exception as e: + status = {"status": "unhealthy", "last_check": datetime.now(UTC).isoformat(), "error": str(e)} + + self.component_status["deadlock_prevention"] = status + self.last_check_time["deadlock_prevention"] = datetime.now(UTC) + return status + + async def full_health_check(self) -> dict[str, Any]: + """Perform full health check of all coordination components.""" + results = {} + + # Run all health checks + results["state_transitions"] = await self.check_state_transitions() + results["coordination"] = await self.check_coordination_protocol() + results["deadlock_prevention"] = await self.check_deadlock_prevention() + + # Overall status + all_statuses = [r["status"] for r in results.values()] + if "unhealthy" in all_statuses: + overall_status = "unhealthy" + elif "warning" in all_statuses: + overall_status = "warning" + else: + overall_status = "healthy" + + return {"overall_status": overall_status, "timestamp": datetime.now(UTC).isoformat(), "components": results} + + +class GracefulDegradation: + """ + Provides graceful degradation strategies when coordination systems fail. + """ + + @staticmethod + async def fallback_task_assignment(task: Task, available_agents: list) -> str | None: + """Fallback task assignment when coordination protocol fails.""" + try: + if not available_agents: + logger.warning("No agents available for fallback assignment") + return None + + # Simple round-robin fallback + # In production, this might use a hash of task ID for consistency + agent_index = hash(task.id) % len(available_agents) + selected_agent = available_agents[agent_index] + + logger.info(f"Fallback assignment: task {task.id} -> agent {selected_agent}") + return selected_agent + + except Exception as e: + logger.error(f"Fallback assignment failed: {e}") + return None + + @staticmethod + async def emergency_conflict_resolution(task: Task, conflicting_versions: list) -> Task: + """Emergency conflict resolution when normal protocols fail.""" + try: + # Extremely simple: take the highest version number + latest_task = max(conflicting_versions, key=lambda t: t.version) + + # Create new task with incremented version + resolved_task = Task( + id=latest_task.id, + title=latest_task.title, + description=latest_task.description, + state=latest_task.state, + priority=latest_task.priority, + estimated_effort=latest_task.estimated_effort, + assigned_to=latest_task.assigned_to, + dependencies=latest_task.dependencies, + metadata=latest_task.metadata, + parent_id=latest_task.parent_id, + subtask_ids=latest_task.subtask_ids, + created_at=latest_task.created_at, + updated_at=datetime.now(UTC), + started_at=latest_task.started_at, + completed_at=latest_task.completed_at, + cancelled_at=latest_task.cancelled_at, + blocked_at=latest_task.blocked_at, + blocking_reason=latest_task.blocking_reason, + version=latest_task.version + 1, + ) + + logger.warning(f"Emergency conflict resolution applied to task {task.id}") + return resolved_task + + except Exception as e: + logger.error(f"Emergency conflict resolution failed: {e}") + raise + + +# Global instances for defensive utilities +file_io = DefensiveFileIO() +health_monitor = CoordinationHealthCheck() diff --git a/amplifier/planner/protocols/state_transitions.py b/amplifier/planner/protocols/state_transitions.py new file mode 100644 index 00000000..535ebf68 --- /dev/null +++ b/amplifier/planner/protocols/state_transitions.py @@ -0,0 +1,238 @@ +""" +State Transition Management Protocol + +Defines valid state transitions, guard conditions, and atomic state change operations +for the super-planner task management system. Ensures data integrity and prevents +invalid state changes through defensive programming patterns. +""" + +import logging +from collections.abc import Callable +from dataclasses import dataclass +from datetime import UTC +from datetime import datetime + +from ..core.models import Task +from ..core.models import TaskState + +logger = logging.getLogger(__name__) + + +class TransitionError(Exception): + """Raised when an invalid state transition is attempted.""" + + def __init__(self, message: str = "Invalid state transition attempted") -> None: + super().__init__(message) + + +class ConcurrencyError(Exception): + """Raised when a concurrent modification is detected.""" + + def __init__(self, message: str = "Concurrent modification detected") -> None: + super().__init__(message) + + +@dataclass(frozen=True) +class StateTransition: + """Represents a state transition with validation and effects.""" + + from_state: TaskState + to_state: TaskState + guard_condition: Callable[[Task], bool] | None = None + side_effects: Callable[[Task], None] | None = None + requires_assignment: bool = False + description: str = "" + + def __hash__(self) -> int: + """Make StateTransition hashable for use in sets.""" + return hash((self.from_state, self.to_state, self.requires_assignment, self.description)) + + +class StateTransitionProtocol: + """ + Manages valid state transitions for tasks with atomic operations, + optimistic locking, and defensive error handling. + """ + + def __init__(self): + self.transitions = self._define_valid_transitions() + self.transition_map = self._build_transition_map() + + def _define_valid_transitions(self) -> set[StateTransition]: + """Define all valid state transitions with their conditions.""" + return { + # Initial transitions from NOT_STARTED + StateTransition( + TaskState.NOT_STARTED, + TaskState.ASSIGNED, + guard_condition=lambda t: t.assigned_to is not None, + description="Assign task to agent", + ), + # Direct start without assignment (for human tasks) + StateTransition( + TaskState.NOT_STARTED, + TaskState.IN_PROGRESS, + guard_condition=lambda t: t.assigned_to is not None, + side_effects=lambda t: setattr(t, "started_at", datetime.now(UTC)), + description="Start task directly", + ), + # From ASSIGNED + StateTransition( + TaskState.ASSIGNED, + TaskState.IN_PROGRESS, + side_effects=lambda t: setattr(t, "started_at", datetime.now(UTC)), + description="Begin working on assigned task", + ), + StateTransition( + TaskState.ASSIGNED, + TaskState.CANCELLED, + side_effects=lambda t: setattr(t, "cancelled_at", datetime.now(UTC)), + description="Cancel assigned task", + ), + # From IN_PROGRESS + StateTransition( + TaskState.IN_PROGRESS, + TaskState.COMPLETED, + side_effects=lambda t: setattr(t, "completed_at", datetime.now(UTC)), + description="Complete task", + ), + StateTransition( + TaskState.IN_PROGRESS, + TaskState.BLOCKED, + guard_condition=lambda t: bool(t.blocking_reason), + side_effects=lambda t: setattr(t, "blocked_at", datetime.now(UTC)), + description="Block task due to dependency or issue", + ), + StateTransition( + TaskState.IN_PROGRESS, + TaskState.CANCELLED, + side_effects=lambda t: setattr(t, "cancelled_at", datetime.now(UTC)), + description="Cancel in-progress task", + ), + # From BLOCKED + StateTransition( + TaskState.BLOCKED, + TaskState.IN_PROGRESS, + guard_condition=lambda t: not t.blocking_reason or t.blocking_reason.strip() == "", + side_effects=lambda t: setattr(t, "blocked_at", None), + description="Unblock and resume task", + ), + StateTransition( + TaskState.BLOCKED, + TaskState.CANCELLED, + side_effects=lambda t: setattr(t, "cancelled_at", datetime.now(UTC)), + description="Cancel blocked task", + ), + # Recovery transitions (for error handling) + StateTransition( + TaskState.ASSIGNED, + TaskState.NOT_STARTED, + side_effects=lambda t: setattr(t, "assigned_to", None), + description="Unassign task (recovery)", + ), + } + + def _build_transition_map(self) -> dict[tuple, StateTransition]: + """Build fast lookup map for transitions.""" + return {(t.from_state, t.to_state): t for t in self.transitions} + + def is_valid_transition(self, from_state: TaskState, to_state: TaskState) -> bool: + """Check if a state transition is valid.""" + return (from_state, to_state) in self.transition_map + + def can_transition(self, task: Task, to_state: TaskState) -> tuple[bool, str | None]: + """ + Check if a task can transition to the given state. + Returns (can_transition, reason_if_not). + """ + if task.state == to_state: + return True, None # Already in target state + + transition_key = (task.state, to_state) + if transition_key not in self.transition_map: + return False, f"No valid transition from {task.state} to {to_state}" + + transition = self.transition_map[transition_key] + + # Check guard condition if present + if transition.guard_condition and not transition.guard_condition(task): + return False, f"Guard condition failed for transition to {to_state}" + + return True, None + + def transition_task(self, task: Task, to_state: TaskState, expected_version: int | None = None) -> Task: + """ + Atomically transition a task to a new state with optimistic locking. + + Args: + task: Task to transition + to_state: Target state + expected_version: Expected version for optimistic locking + + Returns: + Updated task with new state and incremented version + + Raises: + TransitionError: If transition is invalid + ConcurrencyError: If version conflict detected + """ + # Optimistic locking check + if expected_version is not None and task.version != expected_version: + raise ConcurrencyError(f"Version conflict: expected {expected_version}, got {task.version}") + + # Validate transition + can_transition, reason = self.can_transition(task, to_state) + if not can_transition: + raise TransitionError(f"Cannot transition task {task.id}: {reason}") + + # Get transition definition + transition = self.transition_map[(task.state, to_state)] + + # Create updated task (immutable approach) + updated_task = Task( + id=task.id, + title=task.title, + description=task.description, + state=to_state, # New state + priority=task.priority, + estimated_effort=task.estimated_effort, + assigned_to=task.assigned_to, + dependencies=task.dependencies, + metadata=task.metadata.copy() if task.metadata else {}, + parent_id=task.parent_id, + subtask_ids=task.subtask_ids.copy() if task.subtask_ids else [], + created_at=task.created_at, + updated_at=datetime.now(UTC), # Update timestamp + started_at=task.started_at, + completed_at=task.completed_at, + cancelled_at=task.cancelled_at, + blocked_at=task.blocked_at, + blocking_reason=task.blocking_reason, + version=task.version + 1, # Increment version + ) + + # Apply side effects if present + if transition.side_effects: + try: + transition.side_effects(updated_task) + except Exception as e: + logger.error(f"Side effect failed for task {task.id}: {e}") + raise TransitionError(f"Transition side effect failed: {e}") + + logger.info(f"Task {task.id} transitioned from {task.state} to {to_state}") + return updated_task + + def get_valid_next_states(self, current_state: TaskState) -> set[TaskState]: + """Get all valid next states from the current state.""" + return {to_state for (from_state, to_state) in self.transition_map if from_state == current_state} + + def get_transition_description(self, from_state: TaskState, to_state: TaskState) -> str | None: + """Get human-readable description of a transition.""" + transition_key = (from_state, to_state) + if transition_key in self.transition_map: + return self.transition_map[transition_key].description + return None + + +# Global instance +state_protocol = StateTransitionProtocol() diff --git a/amplifier/planner/storage.py b/amplifier/planner/storage.py new file mode 100644 index 00000000..e1150a0d --- /dev/null +++ b/amplifier/planner/storage.py @@ -0,0 +1,70 @@ +"""JSON storage operations for Super-Planner.""" + +from datetime import datetime +from pathlib import Path + +from amplifier.planner.models import Project +from amplifier.planner.models import Task +from amplifier.planner.models import TaskState +from amplifier.utils.file_io import read_json +from amplifier.utils.file_io import write_json + + +def get_project_path(project_id: str) -> Path: + """Get the storage path for a project.""" + base = Path("data/planner/projects") + base.mkdir(parents=True, exist_ok=True) + return base / f"{project_id}.json" + + +def save_project(project: Project) -> None: + """Save project to JSON file.""" + data = { + "id": project.id, + "name": project.name, + "created_at": project.created_at.isoformat(), + "updated_at": project.updated_at.isoformat(), + "tasks": { + task_id: { + "id": task.id, + "title": task.title, + "description": task.description, + "state": task.state.value, + "parent_id": task.parent_id, + "depends_on": task.depends_on, + "assigned_to": task.assigned_to, + "created_at": task.created_at.isoformat(), + "updated_at": task.updated_at.isoformat(), + } + for task_id, task in project.tasks.items() + }, + } + write_json(data, get_project_path(project.id)) + + +def load_project(project_id: str) -> Project: + """Load project from JSON file.""" + data = read_json(get_project_path(project_id)) + + # Reconstruct tasks with proper TaskState enum + tasks = {} + for task_id, task_data in data.get("tasks", {}).items(): + tasks[task_id] = Task( + id=task_data["id"], + title=task_data["title"], + description=task_data["description"], + state=TaskState(task_data["state"]), + parent_id=task_data["parent_id"], + depends_on=task_data["depends_on"], + assigned_to=task_data["assigned_to"], + created_at=datetime.fromisoformat(task_data["created_at"]), + updated_at=datetime.fromisoformat(task_data["updated_at"]), + ) + + return Project( + id=data["id"], + name=data["name"], + tasks=tasks, + created_at=datetime.fromisoformat(data["created_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + ) diff --git a/amplifier/planner/tests/__init__.py b/amplifier/planner/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/amplifier/planner/tests/test_decomposer.py b/amplifier/planner/tests/test_decomposer.py new file mode 100644 index 00000000..b3424665 --- /dev/null +++ b/amplifier/planner/tests/test_decomposer.py @@ -0,0 +1,182 @@ +"""Tests for the task decomposer module.""" + +from unittest.mock import AsyncMock +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest + +from amplifier.planner.decomposer import ProjectContext +from amplifier.planner.decomposer import decompose_goal +from amplifier.planner.decomposer import decompose_recursively +from amplifier.planner.models import Project +from amplifier.planner.models import Task + + +class TestDecomposer: + """Test task decomposition functionality.""" + + @pytest.fixture + def sample_project(self): + """Create a sample project for testing.""" + return Project(id="test-proj", name="Test Project") + + @pytest.fixture + def sample_context(self, sample_project): + """Create a sample project context.""" + return ProjectContext(project=sample_project, max_depth=2, min_tasks=2) + + def test_project_context_creation(self, sample_project): + """Test ProjectContext dataclass creation.""" + context = ProjectContext(project=sample_project) + + assert context.project == sample_project + assert context.max_depth == 3 # default + assert context.min_tasks == 2 # default + assert context.parent_task is None + + def test_project_context_with_parent(self, sample_project): + """Test ProjectContext with parent task.""" + parent = Task(id="parent-1", title="Parent Task") + context = ProjectContext(project=sample_project, parent_task=parent, max_depth=5, min_tasks=3) + + assert context.parent_task == parent + assert context.max_depth == 5 + assert context.min_tasks == 3 + + @pytest.mark.asyncio + async def test_decompose_goal_validation(self, sample_context): + """Test goal validation in decompose_goal.""" + # Test empty goal + with pytest.raises(ValueError, match="Goal cannot be empty"): + await decompose_goal("", sample_context) + + # Test whitespace-only goal + with pytest.raises(ValueError, match="Goal cannot be empty"): + await decompose_goal(" ", sample_context) + + @pytest.mark.asyncio + async def test_decompose_goal_success(self, sample_context): + """Test successful goal decomposition with mocked LLM.""" + mock_decomposition = MagicMock() + mock_decomposition.tasks = [ + {"title": "Task 1", "description": "First task", "depends_on_indices": []}, + {"title": "Task 2", "description": "Second task", "depends_on_indices": [0]}, + {"title": "Task 3", "description": "Third task", "depends_on_indices": [0, 1]}, + ] + + mock_result = MagicMock() + mock_result.output = mock_decomposition + + with patch("amplifier.planner.decomposer._get_decomposer_agent") as mock_agent: + agent = AsyncMock() + agent.run.return_value = mock_result + mock_agent.return_value = agent + + tasks = await decompose_goal("Build a web application", sample_context) + + assert len(tasks) == 3 + assert all(isinstance(t, Task) for t in tasks) + assert tasks[0].title == "Task 1" + assert tasks[1].title == "Task 2" + assert tasks[2].title == "Task 3" + + # Check dependencies + assert len(tasks[0].depends_on) == 0 + assert len(tasks[1].depends_on) == 1 + assert tasks[1].depends_on[0] == tasks[0].id + assert len(tasks[2].depends_on) == 2 + + @pytest.mark.asyncio + async def test_decompose_goal_min_tasks_retry(self, sample_context): + """Test retry logic when minimum tasks not met.""" + sample_context.min_tasks = 3 + + # First response with too few tasks + mock_decomposition_1 = MagicMock() + mock_decomposition_1.tasks = [{"title": "Task 1", "description": "Only task"}] + + # Second response with enough tasks + mock_decomposition_2 = MagicMock() + mock_decomposition_2.tasks = [ + {"title": "Task 1", "description": "First task"}, + {"title": "Task 2", "description": "Second task"}, + {"title": "Task 3", "description": "Third task"}, + ] + + mock_result_1 = MagicMock() + mock_result_1.output = mock_decomposition_1 + mock_result_2 = MagicMock() + mock_result_2.output = mock_decomposition_2 + + with patch("amplifier.planner.decomposer._get_decomposer_agent") as mock_agent: + agent = AsyncMock() + agent.run.side_effect = [mock_result_1, mock_result_2] + mock_agent.return_value = agent + + tasks = await decompose_goal("Simple task", sample_context) + + assert len(tasks) == 3 + assert agent.run.call_count == 2 # Should retry once + + @pytest.mark.asyncio + async def test_decompose_recursively(self, sample_context): + """Test recursive decomposition with mocked responses.""" + sample_context.max_depth = 2 + + # Mock responses for different levels + level1_decomposition = MagicMock() + level1_decomposition.tasks = [ + {"title": "Design system", "description": "Design the system"}, + {"title": "Implement backend", "description": "Build backend"}, + ] + + level2_decomposition = MagicMock() + level2_decomposition.tasks = [ + {"title": "Create database schema", "description": "Design DB"}, + {"title": "Write API endpoints", "description": "Create APIs"}, + ] + + mock_results = [MagicMock(output=level1_decomposition), MagicMock(output=level2_decomposition)] + + with patch("amplifier.planner.decomposer._get_decomposer_agent") as mock_agent: + agent = AsyncMock() + agent.run.side_effect = mock_results + mock_agent.return_value = agent + + with patch("amplifier.planner.decomposer._is_atomic_task") as mock_atomic: + # Return values for each task check (need enough for all tasks) + mock_atomic.side_effect = [False, True, True, True] # First not atomic, rest are + + tasks = await decompose_recursively("Build app", sample_context) + + # Should have tasks from both levels + assert len(tasks) == 4 # 2 from level 1 + 2 from level 2 of first task + + def test_is_atomic_task(self): + """Test atomic task detection.""" + from amplifier.planner.decomposer import _is_atomic_task + + # Atomic tasks + atomic_tasks = [ + Task(id="1", title="Write unit tests"), + Task(id="2", title="Create file structure"), + Task(id="3", title="Implement login function"), + Task(id="4", title="Fix bug in parser"), + Task(id="5", title="Update configuration"), + Task(id="6", title="Delete old files"), + Task(id="7", title="Install dependencies"), + ] + + for task in atomic_tasks: + assert _is_atomic_task(task), f"{task.title} should be atomic" + + # Non-atomic tasks + non_atomic_tasks = [ + Task(id="8", title="Build authentication system"), + Task(id="9", title="Design user interface"), + Task(id="10", title="Develop API layer"), + ] + + for task in non_atomic_tasks: + assert not _is_atomic_task(task), f"{task.title} should not be atomic" diff --git a/amplifier/planner/tests/test_orchestrator.py b/amplifier/planner/tests/test_orchestrator.py new file mode 100644 index 00000000..b4e10632 --- /dev/null +++ b/amplifier/planner/tests/test_orchestrator.py @@ -0,0 +1,221 @@ +"""Tests for the orchestrator module.""" + +from datetime import datetime + +import pytest + +from amplifier.planner.models import Project +from amplifier.planner.models import Task +from amplifier.planner.orchestrator import ExecutionResults +from amplifier.planner.orchestrator import TaskResult +from amplifier.planner.orchestrator import orchestrate_execution + + +@pytest.mark.asyncio +async def test_orchestrate_empty_project(): + """Test orchestrating an empty project.""" + project = Project(id="test-empty", name="Empty Project") + results = await orchestrate_execution(project) + + assert results.project_id == "test-empty" + assert results.status == "completed" + assert results.total_tasks == 0 + assert results.completed_tasks == 0 + assert results.failed_tasks == 0 + + +@pytest.mark.asyncio +async def test_orchestrate_single_task(): + """Test orchestrating a project with a single task.""" + project = Project(id="test-single", name="Single Task Project") + task = Task(id="task1", title="First Task", description="Do something") + project.add_task(task) + + results = await orchestrate_execution(project) + + assert results.project_id == "test-single" + assert results.status == "completed" + assert results.total_tasks == 1 + assert results.completed_tasks == 1 + assert results.failed_tasks == 0 + assert "task1" in results.task_results + assert results.task_results["task1"].status == "success" + + +@pytest.mark.asyncio +async def test_orchestrate_parallel_tasks(): + """Test orchestrating parallel tasks without dependencies.""" + project = Project(id="test-parallel", name="Parallel Tasks") + + # Add multiple independent tasks + for i in range(3): + task = Task(id=f"task{i}", title=f"Task {i}", description=f"Do task {i}") + project.add_task(task) + + results = await orchestrate_execution(project, max_parallel=2) + + assert results.status == "completed" + assert results.total_tasks == 3 + assert results.completed_tasks == 3 + assert all(r.status == "success" for r in results.task_results.values()) + + +@pytest.mark.asyncio +async def test_orchestrate_with_dependencies(): + """Test orchestrating tasks with dependencies.""" + project = Project(id="test-deps", name="Dependent Tasks") + + # Create a chain: task1 -> task2 -> task3 + task1 = Task(id="task1", title="First", description="Do first") + task2 = Task(id="task2", title="Second", description="Do second", depends_on=["task1"]) + task3 = Task(id="task3", title="Third", description="Do third", depends_on=["task2"]) + + project.add_task(task1) + project.add_task(task2) + project.add_task(task3) + + results = await orchestrate_execution(project) + + assert results.status == "completed" + assert results.completed_tasks == 3 + + # Verify execution order (task1 should complete before task2, etc.) + task1_result = results.task_results["task1"] + task2_result = results.task_results["task2"] + task3_result = results.task_results["task3"] + + assert task1_result.completed_at is not None + assert task2_result.completed_at is not None + assert task1_result.completed_at < task2_result.started_at + assert task2_result.completed_at < task3_result.started_at + + +@pytest.mark.asyncio +async def test_orchestrate_complex_dependencies(): + """Test orchestrating with complex dependency graph.""" + project = Project(id="test-complex", name="Complex Dependencies") + + # Create a diamond dependency graph: + # task1 + # / \ + # task2 task3 + # \ / + # task4 + + task1 = Task(id="task1", title="Root", description="Root task") + task2 = Task(id="task2", title="Left", description="Left branch", depends_on=["task1"]) + task3 = Task(id="task3", title="Right", description="Right branch", depends_on=["task1"]) + task4 = Task(id="task4", title="Join", description="Join task", depends_on=["task2", "task3"]) + + project.add_task(task1) + project.add_task(task2) + project.add_task(task3) + project.add_task(task4) + + results = await orchestrate_execution(project, max_parallel=2) + + assert results.status == "completed" + assert results.completed_tasks == 4 + + # Task4 should start after both task2 and task3 complete + task2_result = results.task_results["task2"] + task3_result = results.task_results["task3"] + task4_result = results.task_results["task4"] + + assert task2_result.completed_at is not None + assert task3_result.completed_at is not None + assert task2_result.completed_at < task4_result.started_at + assert task3_result.completed_at < task4_result.started_at + + +@pytest.mark.asyncio +async def test_task_result_tracking(): + """Test that TaskResult properly tracks execution details.""" + result = TaskResult(task_id="test", status="success") + + assert result.task_id == "test" + assert result.status == "success" + assert result.attempts == 1 + assert isinstance(result.started_at, datetime) + assert result.completed_at is None + + # Update result + result.completed_at = datetime.now() + result.output = {"data": "test output"} + + assert result.completed_at is not None + assert result.output == {"data": "test output"} + + +def test_execution_results_counters(): + """Test ExecutionResults counter methods.""" + results = ExecutionResults(project_id="test", status="in_progress", total_tasks=5) + + # Add successful task + results.add_result(TaskResult(task_id="t1", status="success")) + assert results.completed_tasks == 1 + assert results.failed_tasks == 0 + assert results.skipped_tasks == 0 + + # Add failed task + results.add_result(TaskResult(task_id="t2", status="failed")) + assert results.completed_tasks == 1 + assert results.failed_tasks == 1 + assert results.skipped_tasks == 0 + + # Add skipped task + results.add_result(TaskResult(task_id="t3", status="skipped")) + assert results.completed_tasks == 1 + assert results.failed_tasks == 1 + assert results.skipped_tasks == 1 + + # Finalize with partial completion + results.finalize() + assert results.status == "partial" + assert results.completed_at is not None + + +def test_execution_results_finalization(): + """Test ExecutionResults status finalization logic.""" + # All tasks completed + results = ExecutionResults(project_id="test", status="in_progress", total_tasks=2) + results.add_result(TaskResult(task_id="t1", status="success")) + results.add_result(TaskResult(task_id="t2", status="success")) + results.finalize() + assert results.status == "completed" + + # Some tasks failed but some completed + results = ExecutionResults(project_id="test", status="in_progress", total_tasks=3) + results.add_result(TaskResult(task_id="t1", status="success")) + results.add_result(TaskResult(task_id="t2", status="failed")) + results.add_result(TaskResult(task_id="t3", status="skipped")) + results.finalize() + assert results.status == "partial" + + # All tasks failed + results = ExecutionResults(project_id="test", status="in_progress", total_tasks=2) + results.add_result(TaskResult(task_id="t1", status="failed")) + results.add_result(TaskResult(task_id="t2", status="skipped")) + results.finalize() + assert results.status == "failed" + + +@pytest.mark.asyncio +async def test_orchestrate_with_assigned_agents(): + """Test orchestrating tasks with assigned agents.""" + project = Project(id="test-agents", name="Agent Assignment Test") + + task1 = Task(id="task1", title="Code Review", assigned_to="code-reviewer") + task2 = Task(id="task2", title="Bug Fix", assigned_to="bug-fixer", depends_on=["task1"]) + + project.add_task(task1) + project.add_task(task2) + + results = await orchestrate_execution(project) + + assert results.status == "completed" + assert results.completed_tasks == 2 + + # Check that agent assignments were used + assert "code-reviewer" in results.task_results["task1"].output + assert "bug-fixer" in results.task_results["task2"].output diff --git a/amplifier/planner/tests/test_planner.py b/amplifier/planner/tests/test_planner.py new file mode 100644 index 00000000..4f0192e5 --- /dev/null +++ b/amplifier/planner/tests/test_planner.py @@ -0,0 +1,425 @@ +"""Comprehensive tests for the Super-Planner system.""" + +import tempfile +from datetime import UTC +from datetime import datetime +from pathlib import Path + +import pytest + +from amplifier.planner import Project +from amplifier.planner import Task +from amplifier.planner import TaskState +from amplifier.planner import load_project +from amplifier.planner import save_project + + +class TestTask: + """Test Task functionality.""" + + def test_task_creation(self): + """Test basic task creation.""" + task = Task(id="t1", title="Test Task", description="A test task") + assert task.id == "t1" + assert task.title == "Test Task" + assert task.description == "A test task" + assert task.state == TaskState.PENDING + assert task.parent_id is None + assert task.depends_on == [] + assert task.assigned_to is None + assert isinstance(task.created_at, datetime) + assert isinstance(task.updated_at, datetime) + + def test_task_with_all_fields(self): + """Test task creation with all fields.""" + now = datetime.now() + task = Task( + id="t2", + title="Complex Task", + description="A complex task", + state=TaskState.IN_PROGRESS, + parent_id="t1", + depends_on=["t0"], + assigned_to="user1", + created_at=now, + updated_at=now, + ) + assert task.state == TaskState.IN_PROGRESS + assert task.parent_id == "t1" + assert task.depends_on == ["t0"] + assert task.assigned_to == "user1" + + def test_can_start_no_dependencies(self): + """Test can_start with no dependencies.""" + task = Task(id="t1", title="Independent Task") + assert task.can_start(set()) is True + assert task.can_start({"t0"}) is True + + def test_can_start_with_dependencies(self): + """Test can_start with dependencies.""" + task = Task(id="t1", title="Dependent Task", depends_on=["t0", "t2"]) + assert task.can_start(set()) is False + assert task.can_start({"t0"}) is False + assert task.can_start({"t0", "t2"}) is True + assert task.can_start({"t0", "t2", "t3"}) is True + + def test_task_state_values(self): + """Test all task state values.""" + assert TaskState.PENDING.value == "pending" + assert TaskState.IN_PROGRESS.value == "in_progress" + assert TaskState.COMPLETED.value == "completed" + assert TaskState.BLOCKED.value == "blocked" + + +class TestProject: + """Test Project functionality.""" + + def test_project_creation(self): + """Test basic project creation.""" + project = Project(id="p1", name="Test Project") + assert project.id == "p1" + assert project.name == "Test Project" + assert project.tasks == {} + assert isinstance(project.created_at, datetime) + assert isinstance(project.updated_at, datetime) + + def test_add_task(self): + """Test adding tasks to project.""" + project = Project(id="p1", name="Test Project") + original_updated = project.updated_at + + # Small delay to ensure timestamp difference + import time + + time.sleep(0.01) + + task1 = Task(id="t1", title="Task 1") + project.add_task(task1) + + assert "t1" in project.tasks + assert project.tasks["t1"] == task1 + assert project.updated_at > original_updated + + task2 = Task(id="t2", title="Task 2") + project.add_task(task2) + assert len(project.tasks) == 2 + + def test_get_roots(self): + """Test getting root tasks.""" + project = Project(id="p1", name="Test Project") + + # Add root tasks + root1 = Task(id="r1", title="Root 1") + root2 = Task(id="r2", title="Root 2") + project.add_task(root1) + project.add_task(root2) + + # Add child tasks + child1 = Task(id="c1", title="Child 1", parent_id="r1") + child2 = Task(id="c2", title="Child 2", parent_id="r2") + project.add_task(child1) + project.add_task(child2) + + roots = project.get_roots() + assert len(roots) == 2 + assert root1 in roots + assert root2 in roots + assert child1 not in roots + assert child2 not in roots + + def test_get_children(self): + """Test getting child tasks.""" + project = Project(id="p1", name="Test Project") + + # Add parent task + parent = Task(id="parent", title="Parent Task") + project.add_task(parent) + + # Add children + child1 = Task(id="c1", title="Child 1", parent_id="parent") + child2 = Task(id="c2", title="Child 2", parent_id="parent") + child3 = Task(id="c3", title="Child 3", parent_id="other") + project.add_task(child1) + project.add_task(child2) + project.add_task(child3) + + children = project.get_children("parent") + assert len(children) == 2 + assert child1 in children + assert child2 in children + assert child3 not in children + + def test_hierarchical_structure(self): + """Test complex hierarchical task structure.""" + project = Project(id="p1", name="Hierarchical Project") + + # Create hierarchy: + # root1 + # β”œβ”€β”€ child1 + # β”‚ └── grandchild1 + # └── child2 + # root2 + + root1 = Task(id="r1", title="Root 1") + root2 = Task(id="r2", title="Root 2") + child1 = Task(id="c1", title="Child 1", parent_id="r1") + child2 = Task(id="c2", title="Child 2", parent_id="r1") + grandchild1 = Task(id="gc1", title="Grandchild 1", parent_id="c1") + + for task in [root1, root2, child1, child2, grandchild1]: + project.add_task(task) + + # Test structure + roots = project.get_roots() + assert len(roots) == 2 + + r1_children = project.get_children("r1") + assert len(r1_children) == 2 + assert child1 in r1_children + assert child2 in r1_children + + c1_children = project.get_children("c1") + assert len(c1_children) == 1 + assert grandchild1 in c1_children + + r2_children = project.get_children("r2") + assert len(r2_children) == 0 + + +class TestStorage: + """Test storage operations.""" + + @pytest.fixture + def temp_data_dir(self, monkeypatch): + """Use temporary directory for data storage.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir) + + # Patch the get_project_path to use temp directory + def mock_get_path(project_id): + base = tmpdir_path / "data" / "planner" / "projects" + base.mkdir(parents=True, exist_ok=True) + return base / f"{project_id}.json" + + monkeypatch.setattr("amplifier.planner.storage.get_project_path", mock_get_path) + yield tmpdir_path + + def test_save_and_load_simple(self, temp_data_dir): + """Test saving and loading a simple project.""" + project = Project(id="p1", name="Test Project") + task = Task(id="t1", title="Test Task", description="Description") + project.add_task(task) + + # Save project + save_project(project) + + # Load project + loaded = load_project("p1") + + assert loaded.id == "p1" + assert loaded.name == "Test Project" + assert len(loaded.tasks) == 1 + assert "t1" in loaded.tasks + assert loaded.tasks["t1"].title == "Test Task" + assert loaded.tasks["t1"].description == "Description" + + def test_save_and_load_complex(self, temp_data_dir): + """Test saving and loading project with all task fields.""" + project = Project(id="p2", name="Complex Project") + + # Create tasks with various states and relationships + task1 = Task( + id="t1", + title="Task 1", + description="First task", + state=TaskState.COMPLETED, + assigned_to="user1", + ) + + task2 = Task( + id="t2", + title="Task 2", + description="Second task", + state=TaskState.IN_PROGRESS, + parent_id="t1", + depends_on=["t1"], + assigned_to="user2", + ) + + task3 = Task( + id="t3", + title="Task 3", + state=TaskState.BLOCKED, + depends_on=["t1", "t2"], + ) + + project.add_task(task1) + project.add_task(task2) + project.add_task(task3) + + # Save and reload + save_project(project) + loaded = load_project("p2") + + # Verify all data preserved + assert len(loaded.tasks) == 3 + + # Check task1 + t1 = loaded.tasks["t1"] + assert t1.title == "Task 1" + assert t1.state == TaskState.COMPLETED + assert t1.assigned_to == "user1" + + # Check task2 + t2 = loaded.tasks["t2"] + assert t2.state == TaskState.IN_PROGRESS + assert t2.parent_id == "t1" + assert t2.depends_on == ["t1"] + assert t2.assigned_to == "user2" + + # Check task3 + t3 = loaded.tasks["t3"] + assert t3.state == TaskState.BLOCKED + assert t3.depends_on == ["t1", "t2"] + + def test_round_trip_preserves_timestamps(self, temp_data_dir): + """Test that timestamps are preserved through save/load.""" + now = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) + + project = Project(id="p3", name="Timestamp Test", created_at=now, updated_at=now) + + task = Task( + id="t1", + title="Task", + created_at=now, + updated_at=now, + ) + project.tasks["t1"] = task # Add directly to avoid updating timestamp + + save_project(project) + loaded = load_project("p3") + + assert loaded.created_at == now + assert loaded.updated_at == now + assert loaded.tasks["t1"].created_at == now + assert loaded.tasks["t1"].updated_at == now + + def test_empty_project(self, temp_data_dir): + """Test saving and loading empty project.""" + project = Project(id="empty", name="Empty Project") + save_project(project) + + loaded = load_project("empty") + assert loaded.id == "empty" + assert loaded.name == "Empty Project" + assert loaded.tasks == {} + + def test_task_state_enum_serialization(self, temp_data_dir): + """Test that TaskState enum values are correctly serialized.""" + project = Project(id="p4", name="Enum Test") + + for state in TaskState: + task = Task(id=state.value, title=f"{state.value} task", state=state) + project.add_task(task) + + save_project(project) + loaded = load_project("p4") + + for state in TaskState: + assert state.value in loaded.tasks + assert loaded.tasks[state.value].state == state + + +class TestIntegration: + """Integration tests for complete workflows.""" + + @pytest.fixture + def temp_data_dir(self, monkeypatch): + """Use temporary directory for data storage.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_path = Path(tmpdir) + + def mock_get_path(project_id): + base = tmpdir_path / "data" / "planner" / "projects" + base.mkdir(parents=True, exist_ok=True) + return base / f"{project_id}.json" + + monkeypatch.setattr("amplifier.planner.storage.get_project_path", mock_get_path) + yield tmpdir_path + + def test_complete_workflow(self, temp_data_dir): + """Test a complete project workflow.""" + # Create project + project = Project(id="workflow", name="Complete Workflow") + + # Add epic (root task) + epic = Task( + id="epic1", + title="Build Feature X", + description="Complete implementation of Feature X", + assigned_to="team-lead", + ) + project.add_task(epic) + + # Add stories under epic + story1 = Task( + id="story1", + title="Design API", + parent_id="epic1", + assigned_to="architect", + ) + story2 = Task( + id="story2", + title="Implement Backend", + parent_id="epic1", + depends_on=["story1"], + assigned_to="backend-dev", + ) + story3 = Task( + id="story3", + title="Create UI", + parent_id="epic1", + depends_on=["story1"], + assigned_to="frontend-dev", + ) + story4 = Task( + id="story4", + title="Integration Testing", + parent_id="epic1", + depends_on=["story2", "story3"], + assigned_to="qa", + ) + + for story in [story1, story2, story3, story4]: + project.add_task(story) + + # Test dependency checking + completed = set() + assert story1.can_start(completed) is True # No dependencies + assert story2.can_start(completed) is False # Depends on story1 + assert story3.can_start(completed) is False # Depends on story1 + + # Complete story1 + completed.add("story1") + story1.state = TaskState.COMPLETED + + assert story2.can_start(completed) is True # Dependencies met + assert story3.can_start(completed) is True # Dependencies met + assert story4.can_start(completed) is False # Needs story2 and story3 + + # Save and reload + save_project(project) + loaded = load_project("workflow") + + # Verify structure preserved + roots = loaded.get_roots() + assert len(roots) == 1 + assert roots[0].id == "epic1" + + children = loaded.get_children("epic1") + assert len(children) == 4 + child_ids = {c.id for c in children} + assert child_ids == {"story1", "story2", "story3", "story4"} + + # Verify dependencies preserved + assert loaded.tasks["story4"].depends_on == ["story2", "story3"] diff --git a/amplifier/planner/tests/test_planner_hostile.py b/amplifier/planner/tests/test_planner_hostile.py new file mode 100644 index 00000000..c0d1f84f --- /dev/null +++ b/amplifier/planner/tests/test_planner_hostile.py @@ -0,0 +1,916 @@ +#!/usr/bin/env python3 +""" +Hostile Testing Suite for Super-Planner Phase 1 +Testing with "Rewrite Vi Editor in Python" - A Complex Multi-Level Project + +This test is designed to break things and find edge cases by: +- Creating deeply nested hierarchies (5+ levels) +- Complex dependency graphs with multiple paths +- Large numbers of tasks (100+) +- Edge case scenarios (circular deps, missing deps, etc.) +- Stress testing state transitions +- Large data serialization +- Unusual task names and characters +- Performance testing with bulk operations +""" + +import json +import sys +import time +from pathlib import Path + +from amplifier.planner import Project +from amplifier.planner import Task +from amplifier.planner import TaskState +from amplifier.planner import load_project +from amplifier.planner import save_project + + +class HostileTester: + """Hostile testing framework for Phase 1""" + + def __init__(self): + self.failures = [] + self.test_count = 0 + self.start_time = time.time() + + def assert_test(self, condition, message): + """Custom assertion with failure tracking""" + self.test_count += 1 + if not condition: + self.failures.append(f"❌ Test {self.test_count}: {message}") + print(f"❌ FAIL: {message}") + return False + print(f"βœ… PASS: {message}") + return True + + def test_edge_case_task_creation(self): + """Test edge cases in task creation""" + print("\nπŸ”₯ HOSTILE TEST 1: Edge Case Task Creation") + + # Test with unusual characters + weird_task = Task( + id="weird-chars-ℒ️πŸ”₯πŸ’»", + title="Task with Γ©mojis and spΓ©ciaΕ‚ chars: <>&\"'", + description="Multi-line\ndescription with\ttabs and\nweird chars: δ½ ε₯½δΈ–η•Œ", + ) + self.assert_test(weird_task.id == "weird-chars-ℒ️πŸ”₯πŸ’»", "Task with weird characters created") + + # Test with very long strings + long_id = "a" * 1000 + long_task = Task(id=long_id, title="b" * 500, description="c" * 2000) + self.assert_test(len(long_task.description) == 2000, "Task with very long strings") + + # Test with empty/minimal data + minimal_task = Task("", "") + self.assert_test(minimal_task.id == "", "Task with empty ID") + + # Test with None values where optional + task_with_nones = Task("test", "Test", assigned_to=None, parent_id=None) + self.assert_test(task_with_nones.assigned_to is None, "Task with explicit None values") + + return True + + def test_circular_dependencies(self): + """Test detection/handling of circular dependencies""" + print("\nπŸ”₯ HOSTILE TEST 2: Circular Dependencies") + + project = Project("circular-test", "Circular Dependency Test") + + # Create circular dependency: A -> B -> C -> A + task_a = Task("a", "Task A", depends_on=["c"]) # Depends on C + task_b = Task("b", "Task B", depends_on=["a"]) # Depends on A + task_c = Task("c", "Task C", depends_on=["b"]) # Depends on B -> Circular! + + project.add_task(task_a) + project.add_task(task_b) + project.add_task(task_c) + + # Test that none can start (circular dependency) + completed = set() + can_start_a = task_a.can_start(completed) + can_start_b = task_b.can_start(completed) + can_start_c = task_c.can_start(completed) + + self.assert_test( + not can_start_a and not can_start_b and not can_start_c, + "Circular dependencies prevent all tasks from starting", + ) + + return project + + def test_complex_dependency_graph(self): + """Test complex dependency resolution""" + print("\nπŸ”₯ HOSTILE TEST 3: Complex Dependency Graph") + + project = Project("complex-deps", "Complex Dependencies") + + # Create diamond dependency pattern + root = Task("root", "Root Task") + branch_a = Task("branch_a", "Branch A", depends_on=["root"]) + branch_b = Task("branch_b", "Branch B", depends_on=["root"]) + merge = Task("merge", "Merge Task", depends_on=["branch_a", "branch_b"]) + + # Create fan-out from merge + fan1 = Task("fan1", "Fan 1", depends_on=["merge"]) + fan2 = Task("fan2", "Fan 2", depends_on=["merge"]) + fan3 = Task("fan3", "Fan 3", depends_on=["merge"]) + + # Create final convergence + final = Task("final", "Final", depends_on=["fan1", "fan2", "fan3"]) + + tasks = [root, branch_a, branch_b, merge, fan1, fan2, fan3, final] + for task in tasks: + project.add_task(task) + + # Test dependency resolution step by step + completed = set() + + # Initially only root can start + startable = [t for t in tasks if t.can_start(completed)] + self.assert_test(len(startable) == 1 and startable[0].id == "root", "Only root task can start initially") + + # Complete root, now branches can start + completed.add("root") + startable = [t for t in tasks if t.can_start(completed) and t.id not in completed] + startable_ids = {t.id for t in startable} + self.assert_test(startable_ids == {"branch_a", "branch_b"}, "After root, both branches can start") + + # Complete both branches, merge can start + completed.update(["branch_a", "branch_b"]) + startable = [t for t in tasks if t.can_start(completed) and t.id not in completed] + self.assert_test(len(startable) == 1 and startable[0].id == "merge", "After branches, merge can start") + + return project + + def create_vi_editor_project(self): + """Create the monster Vi editor rewrite project""" + print("\nπŸ”₯ HOSTILE TEST 4: Vi Editor Project - Ultra Complex Hierarchy") + + project = Project("vi-rewrite", "Vi Editor Rewrite in Python") + + # Level 1: Major components + architecture = Task( + "arch", "Architecture & Design", "Define overall architecture, data structures, and interfaces" + ) + + core_engine = Task( + "core", + "Core Editing Engine", + "Text buffer management, cursor handling, basic operations", + depends_on=["arch"], + ) + + command_system = Task( + "commands", "Command System", "Vi command parsing, execution, and modal interface", depends_on=["arch"] + ) + + ui_system = Task( + "ui", "User Interface System", "Terminal handling, screen rendering, input processing", depends_on=["arch"] + ) + + file_system = Task( + "filesystem", "File Operations", "File I/O, backup, recovery, file type detection", depends_on=["arch"] + ) + + # Level 2: Architecture breakdown + arch_tasks = [ + Task( + "arch-analysis", + "Requirements Analysis", + parent_id="arch", + description="Analyze vi behavior, commands, edge cases", + ), + Task( + "arch-design", + "System Design", + parent_id="arch", + depends_on=["arch-analysis"], + description="Design core data structures and interfaces", + ), + Task( + "arch-patterns", + "Design Patterns", + parent_id="arch", + depends_on=["arch-design"], + description="Define patterns for extensibility", + ), + Task( + "arch-docs", + "Architecture Documentation", + parent_id="arch", + depends_on=["arch-patterns"], + description="Document architecture decisions", + ), + ] + + # Level 2: Core engine breakdown + core_tasks = [ + Task( + "core-buffer", + "Text Buffer", + parent_id="core", + depends_on=["core"], + description="Efficient text storage with undo/redo", + ), + Task( + "core-cursor", + "Cursor Management", + parent_id="core", + depends_on=["core-buffer"], + description="Cursor positioning, movement, selection", + ), + Task( + "core-operations", + "Basic Operations", + parent_id="core", + depends_on=["core-cursor"], + description="Insert, delete, replace operations", + ), + Task( + "core-marks", + "Marks and Registers", + parent_id="core", + depends_on=["core-operations"], + description="Named marks, registers, clipboard", + ), + Task( + "core-search", + "Search Engine", + parent_id="core", + depends_on=["core-operations"], + description="Pattern matching, regex, search/replace", + ), + ] + + # Level 3: Buffer implementation details + buffer_tasks = [ + Task( + "buffer-gap", + "Gap Buffer Implementation", + parent_id="core-buffer", + depends_on=["core-buffer"], + description="Efficient gap buffer with automatic expansion", + ), + Task( + "buffer-lines", + "Line Management", + parent_id="core-buffer", + depends_on=["buffer-gap"], + description="Line indexing, wrapping, virtual lines", + ), + Task( + "buffer-encoding", + "Text Encoding", + parent_id="core-buffer", + depends_on=["buffer-lines"], + description="UTF-8, line endings, BOM handling", + ), + Task( + "buffer-undo", + "Undo System", + parent_id="core-buffer", + depends_on=["buffer-encoding"], + description="Efficient undo/redo with branching", + ), + Task( + "buffer-diff", + "Change Tracking", + parent_id="core-buffer", + depends_on=["buffer-undo"], + description="Track changes for diff, backup", + ), + ] + + # Level 2: Command system breakdown + command_tasks = [ + Task( + "cmd-parser", + "Command Parser", + parent_id="commands", + depends_on=["commands"], + description="Parse vi commands with error handling", + ), + Task( + "cmd-modes", + "Modal Interface", + parent_id="commands", + depends_on=["cmd-parser"], + description="Normal, insert, visual, command modes", + ), + Task( + "cmd-movement", + "Movement Commands", + parent_id="commands", + depends_on=["cmd-modes"], + description="h,j,k,l, w,e,b, $,^, G, etc.", + ), + Task( + "cmd-editing", + "Editing Commands", + parent_id="commands", + depends_on=["cmd-movement"], + description="i,a,o, d,c,y, p, u, ., etc.", + ), + Task( + "cmd-visual", + "Visual Mode", + parent_id="commands", + depends_on=["cmd-editing"], + description="Visual selection, visual block", + ), + Task( + "cmd-ex", + "Ex Commands", + parent_id="commands", + depends_on=["cmd-visual"], + description=":w, :q, :s, :!, etc.", + ), + ] + + # Level 3: Movement command details + movement_tasks = [ + Task( + "move-char", + "Character Movement", + parent_id="cmd-movement", + depends_on=["cmd-movement"], + description="h,l,space,backspace movement", + ), + Task( + "move-word", + "Word Movement", + parent_id="cmd-movement", + depends_on=["move-char"], + description="w,e,b,W,E,B word boundaries", + ), + Task( + "move-line", + "Line Movement", + parent_id="cmd-movement", + depends_on=["move-word"], + description="j,k,$,^,0,+,- movements", + ), + Task( + "move-search", + "Search Movement", + parent_id="cmd-movement", + depends_on=["move-line"], + description="f,F,t,T,;,, and /,? searches", + ), + Task( + "move-jump", + "Jump Commands", + parent_id="cmd-movement", + depends_on=["move-search"], + description="G,gg,H,M,L,ctrl-f,ctrl-b", + ), + ] + + # Level 2: UI system breakdown + ui_tasks = [ + Task( + "ui-terminal", + "Terminal Interface", + parent_id="ui", + depends_on=["ui"], + description="Raw terminal control, escape sequences", + ), + Task( + "ui-screen", + "Screen Management", + parent_id="ui", + depends_on=["ui-terminal"], + description="Screen buffer, scrolling, refresh", + ), + Task( + "ui-input", + "Input Processing", + parent_id="ui", + depends_on=["ui-screen"], + description="Key mapping, special keys, timing", + ), + Task( + "ui-rendering", + "Text Rendering", + parent_id="ui", + depends_on=["ui-input"], + description="Syntax highlighting, line numbers", + ), + Task( + "ui-status", + "Status Line", + parent_id="ui", + depends_on=["ui-rendering"], + description="Mode indicator, position, filename", + ), + ] + + # Level 2: File system breakdown + file_tasks = [ + Task( + "file-io", + "Basic File I/O", + parent_id="filesystem", + depends_on=["filesystem"], + description="Read/write files, error handling", + ), + Task( + "file-backup", + "Backup System", + parent_id="filesystem", + depends_on=["file-io"], + description="Swap files, backup files, recovery", + ), + Task( + "file-detection", + "File Type Detection", + parent_id="filesystem", + depends_on=["file-backup"], + description="Syntax detection, file associations", + ), + Task( + "file-encoding", + "Encoding Detection", + parent_id="filesystem", + depends_on=["file-detection"], + description="Auto-detect and handle encodings", + ), + ] + + # Integration tasks (depend on multiple components) + integration_tasks = [ + Task( + "integration-basic", + "Basic Integration", + depends_on=["core-operations", "cmd-editing", "ui-screen"], + description="Integrate core editing with UI", + ), + Task( + "integration-advanced", + "Advanced Integration", + depends_on=["integration-basic", "core-search", "cmd-ex"], + description="Integrate search, ex commands", + ), + Task( + "integration-files", + "File Integration", + depends_on=["integration-advanced", "file-encoding"], + description="Integrate file operations", + ), + ] + + # Testing tasks (comprehensive testing pyramid) + test_tasks = [ + Task( + "test-unit", + "Unit Tests", + depends_on=["buffer-diff", "move-jump", "ui-status"], + description="Unit tests for all components", + ), + Task( + "test-integration", + "Integration Tests", + depends_on=["test-unit", "integration-files"], + description="Integration tests across components", + ), + Task( + "test-compatibility", + "Vi Compatibility Tests", + depends_on=["test-integration"], + description="Test compatibility with real vi", + ), + Task( + "test-performance", + "Performance Tests", + depends_on=["test-compatibility"], + description="Large file performance, memory usage", + ), + Task( + "test-edge-cases", + "Edge Case Tests", + depends_on=["test-performance"], + description="Binary files, huge files, corner cases", + ), + ] + + # Polish and release tasks + polish_tasks = [ + Task( + "polish-docs", + "Documentation", + depends_on=["test-edge-cases"], + description="User manual, developer docs", + ), + Task( + "polish-package", + "Packaging", + depends_on=["polish-docs"], + description="Setup.py, distribution, installation", + ), + Task( + "polish-ci", + "CI/CD Setup", + depends_on=["polish-package"], + description="GitHub actions, automated testing", + ), + Task("release-beta", "Beta Release", depends_on=["polish-ci"], description="Initial beta release"), + Task("release-stable", "Stable Release", depends_on=["release-beta"], description="1.0 stable release"), + ] + + # Add ALL tasks to project + all_tasks = ( + [architecture, core_engine, command_system, ui_system, file_system] + + arch_tasks + + core_tasks + + buffer_tasks + + command_tasks + + movement_tasks + + ui_tasks + + file_tasks + + integration_tasks + + test_tasks + + polish_tasks + ) + + print(f" Creating {len(all_tasks)} tasks with complex dependencies...") + + for task in all_tasks: + project.add_task(task) + + # Add some stress test tasks with random dependencies + import random + + for i in range(20): + # Pick random existing tasks as dependencies + existing_ids = list(project.tasks.keys()) + num_deps = random.randint(0, min(3, len(existing_ids))) + deps = random.sample(existing_ids, num_deps) if num_deps > 0 else [] + + stress_task = Task( + f"stress-{i:02d}", + f"Stress Test Task {i}", + f"Random stress test task with {num_deps} dependencies", + depends_on=deps, + ) + project.add_task(stress_task) + + print(f" Total tasks created: {len(project.tasks)}") + + self.assert_test(len(project.tasks) > 50, f"Created large project with {len(project.tasks)} tasks") + + return project + + def test_hierarchy_navigation(self, project): + """Test complex hierarchy navigation""" + print("\nπŸ”₯ HOSTILE TEST 5: Complex Hierarchy Navigation") + + # Test root finding + project.get_roots() + + # Find actual roots (tasks with no parent_id and not depended on by others) + actual_roots = [] + for task in project.tasks.values(): + if task.parent_id is None: + # Check if it's not just a dependency + is_dependency_only = any( + task.id in other_task.depends_on + for other_task in project.tasks.values() + if other_task.id != task.id + ) + if not is_dependency_only or task.id in ["arch", "core", "commands", "ui", "filesystem"]: + actual_roots.append(task) + + self.assert_test(len(actual_roots) >= 5, f"Found {len(actual_roots)} root tasks in complex hierarchy") + + # Test deep hierarchy navigation + if "arch" in project.tasks: + arch_children = project.get_children("arch") + self.assert_test(len(arch_children) >= 4, f"Architecture has {len(arch_children)} direct children") + + # Test grandchildren + if arch_children: + first_child = arch_children[0] + grandchildren = project.get_children(first_child.id) + print(f" Found {len(grandchildren)} grandchildren under {first_child.title}") + + # Test that hierarchy doesn't create loops + def check_hierarchy_loops(task_id, visited=None): + if visited is None: + visited = set() + if task_id in visited: + return True # Loop detected + visited.add(task_id) + + task = project.tasks.get(task_id) + if task and task.parent_id: + return check_hierarchy_loops(task.parent_id, visited) + return False + + loop_count = 0 + for task_id in project.tasks: + if check_hierarchy_loops(task_id): + loop_count += 1 + + self.assert_test(loop_count == 0, f"No hierarchy loops detected (checked {len(project.tasks)} tasks)") + + return True + + def test_dependency_resolution_stress(self, project): + """Stress test dependency resolution""" + print("\nπŸ”₯ HOSTILE TEST 6: Dependency Resolution Stress Test") + + # Test can_start for all tasks + completed = set() + start_time = time.time() + + initial_startable = [] + for task in project.tasks.values(): + if task.can_start(completed): + initial_startable.append(task) + + resolution_time = time.time() - start_time + self.assert_test( + resolution_time < 1.0, f"Dependency resolution for {len(project.tasks)} tasks took {resolution_time:.3f}s" + ) + + print(f" Initially {len(initial_startable)} tasks can start") + + # Simulate completing tasks and check resolution at each step + simulation_steps = 0 + max_steps = min(50, len(project.tasks)) # Limit simulation for performance + + while len(completed) < max_steps and simulation_steps < max_steps: + startable = [t for t in project.tasks.values() if t.can_start(completed) and t.id not in completed] + + if not startable: + break + + # "Complete" the first startable task + next_task = startable[0] + completed.add(next_task.id) + simulation_steps += 1 + + if simulation_steps % 10 == 0: + print(f" Step {simulation_steps}: Completed {len(completed)} tasks, {len(startable)} available") + + self.assert_test(simulation_steps > 20, f"Successfully simulated {simulation_steps} task completions") + + # Test that we didn't get stuck (should be able to complete at least some tasks) + progress_ratio = len(completed) / len(project.tasks) + self.assert_test(progress_ratio > 0.1, f"Made progress on {progress_ratio:.2%} of tasks") + + return True + + def test_serialization_stress(self, project): + """Test serialization with large, complex data""" + print("\nπŸ”₯ HOSTILE TEST 7: Serialization Stress Test") + + # Add tasks with extreme data + extreme_task = Task( + "extreme-data", + "Task with extreme data", + "Description with every Unicode character: " + "".join(chr(i) for i in range(32, 127)), + ) + + # Add metadata stress test + extreme_task.assigned_to = "user with πŸ”₯Γ©mojisπŸ’» and spΓ«ciΓ₯l chars" + project.add_task(extreme_task) + + # Test save with large project + start_time = time.time() + try: + save_project(project) + save_time = time.time() - start_time + self.assert_test(save_time < 5.0, f"Large project save took {save_time:.3f}s (under 5s)") + except Exception as e: + self.assert_test(False, f"Save failed with large project: {e}") + return False + + # Test load with large project + start_time = time.time() + try: + loaded_project = load_project(project.id) + load_time = time.time() - start_time + self.assert_test(load_time < 2.0, f"Large project load took {load_time:.3f}s (under 2s)") + except Exception as e: + self.assert_test(False, f"Load failed with large project: {e}") + return False + + # Test data integrity with complex project + self.assert_test(loaded_project is not None, "Large project loaded successfully") + self.assert_test( + len(loaded_project.tasks) == len(project.tasks), f"All {len(project.tasks)} tasks preserved after save/load" + ) + + # Test specific task integrity + if "extreme-data" in loaded_project.tasks: + loaded_extreme = loaded_project.tasks["extreme-data"] + self.assert_test( + loaded_extreme.assigned_to == extreme_task.assigned_to, "Unicode characters preserved in assignment" + ) + + # Test JSON structure manually + data_file = Path("data/planner/projects") / f"{project.id}.json" + if data_file.exists(): + try: + with open(data_file) as f: + json_data = json.load(f) + + self.assert_test("tasks" in json_data, "JSON contains tasks array") + self.assert_test(len(json_data["tasks"]) == len(project.tasks), "JSON task count matches project") + + # Test that JSON is properly formatted (can be re-parsed) + json_str = json.dumps(json_data, indent=2) + reparsed = json.loads(json_str) + self.assert_test(reparsed == json_data, "JSON round-trip successful") + + except Exception as e: + self.assert_test(False, f"JSON validation failed: {e}") + + return loaded_project + + def test_state_transition_stress(self, project): + """Test state transitions under stress""" + print("\nπŸ”₯ HOSTILE TEST 8: State Transition Stress") + + # Test all possible state transitions + test_task = Task("state-test", "State Transition Test") + project.add_task(test_task) + + # Test each state transition + states = [TaskState.PENDING, TaskState.IN_PROGRESS, TaskState.COMPLETED, TaskState.BLOCKED] + + for from_state in states: + for to_state in states: + test_task.state = from_state + test_task.state = to_state # Should always work (no validation in Phase 1) + self.assert_test(test_task.state == to_state, f"Transition from {from_state.value} to {to_state.value}") + + # Test rapid state changes (performance) + start_time = time.time() + for i in range(1000): + test_task.state = states[i % len(states)] + + transition_time = time.time() - start_time + self.assert_test(transition_time < 0.1, f"1000 state transitions took {transition_time:.3f}s") + + # Test state preservation through save/load + test_task.state = TaskState.BLOCKED + save_project(project) + + loaded = load_project(project.id) + if loaded and "state-test" in loaded.tasks: + loaded_state = loaded.tasks["state-test"].state + self.assert_test(loaded_state == TaskState.BLOCKED, "BLOCKED state preserved through save/load") + + return True + + def test_error_conditions(self): + """Test error handling and edge cases""" + print("\nπŸ”₯ HOSTILE TEST 9: Error Conditions & Edge Cases") + + # Test loading non-existent project + try: + load_project("this-project-does-not-exist-12345") + self.assert_test(False, "Loading non-existent project should raise FileNotFoundError") + except FileNotFoundError: + self.assert_test(True, "Loading non-existent project raises FileNotFoundError") + + # Test with invalid task dependencies + project = Project("error-test", "Error Test Project") + + task_with_missing_deps = Task( + "missing-deps", "Missing Dependencies", depends_on=["non-existent-1", "non-existent-2"] + ) + project.add_task(task_with_missing_deps) + + # Should not crash, but can't start due to missing dependencies + completed = set() + can_start = task_with_missing_deps.can_start(completed) + self.assert_test(not can_start, "Task with missing dependencies cannot start") + + # Test adding dependencies that exist + helper_task = Task("helper", "Helper Task") + project.add_task(helper_task) + completed.add("helper") # Complete the helper + + # Still can't start because other deps are missing + still_cant_start = task_with_missing_deps.can_start(completed) + self.assert_test(not still_cant_start, "Task still blocked by remaining missing dependencies") + + # Test duplicate task IDs + original_task = Task("duplicate-id", "Original Task") + duplicate_task = Task("duplicate-id", "Duplicate Task") + + project.add_task(original_task) + project.add_task(duplicate_task) # Should overwrite + + self.assert_test( + project.tasks["duplicate-id"].title == "Duplicate Task", "Duplicate task ID overwrites original" + ) + + return True + + def test_performance_limits(self, project): + """Test performance with large operations""" + print("\nπŸ”₯ HOSTILE TEST 10: Performance Limits") + + # Test bulk operations + bulk_tasks = [] + start_time = time.time() + + for i in range(500): + task = Task(f"bulk-{i:03d}", f"Bulk Task {i}", f"Generated task {i}") + bulk_tasks.append(task) + project.add_task(task) + + bulk_creation_time = time.time() - start_time + self.assert_test(bulk_creation_time < 2.0, f"Creating 500 tasks took {bulk_creation_time:.3f}s") + + print(f" Project now has {len(project.tasks)} total tasks") + + # Test bulk dependency checking + completed = {f"bulk-{i:03d}" for i in range(0, 250, 2)} # Complete every other task + + start_time = time.time() + startable_count = 0 + for task in project.tasks.values(): + if task.can_start(completed): + startable_count += 1 + + dependency_check_time = time.time() - start_time + self.assert_test( + dependency_check_time < 1.0, + f"Dependency check on {len(project.tasks)} tasks took {dependency_check_time:.3f}s", + ) + + print(f" Found {startable_count} startable tasks with {len(completed)} completed") + + # Test memory usage (rough estimate) + import sys + + project_size = sys.getsizeof(project) + sum(sys.getsizeof(task) for task in project.tasks.values()) + size_mb = project_size / (1024 * 1024) + + self.assert_test(size_mb < 10, f"Project memory usage {size_mb:.2f}MB (under 10MB)") + + return True + + def run_all_tests(self): + """Run the complete hostile test suite""" + print("πŸ”₯πŸ”₯πŸ”₯ HOSTILE TESTING SUITE - SUPER-PLANNER PHASE 1 πŸ”₯πŸ”₯πŸ”₯") + print("Designed to break things and find every possible edge case!") + print("=" * 80) + + # Run all tests + self.test_edge_case_task_creation() + self.test_circular_dependencies() + self.test_complex_dependency_graph() + + vi_project = self.create_vi_editor_project() + self.test_hierarchy_navigation(vi_project) + self.test_dependency_resolution_stress(vi_project) + loaded_project = self.test_serialization_stress(vi_project) + self.test_state_transition_stress(loaded_project or vi_project) + self.test_error_conditions() + self.test_performance_limits(loaded_project or vi_project) + + # Final results + total_time = time.time() - self.start_time + + print("\n" + "=" * 80) + print("πŸ”₯ HOSTILE TESTING COMPLETE πŸ”₯") + print(f"⏱️ Total time: {total_time:.2f} seconds") + print(f"πŸ§ͺ Tests run: {self.test_count}") + print(f"βœ… Passed: {self.test_count - len(self.failures)}") + print(f"❌ Failed: {len(self.failures)}") + + if self.failures: + print("\nπŸ’₯ FAILURES:") + for failure in self.failures: + print(f" {failure}") + print("\n🚨 PHASE 1 HAS ISSUES! 🚨") + return False + print("\nπŸŽ‰ ALL HOSTILE TESTS PASSED! πŸŽ‰") + print("✨ Phase 1 is BULLETPROOF! ✨") + print( + f"πŸ’ͺ Successfully handled {len(loaded_project.tasks) if 'loaded_project' in locals() and loaded_project else 'many'} tasks in complex project" + ) + return True + + +def main(): + """Run the hostile test suite""" + tester = HostileTester() + success = tester.run_all_tests() + + print("\nπŸ“ Test data saved to: data/planner/projects/") + print(" Check vi-rewrite.json to see the complex project structure") + + return success + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/amplifier/planner/tests/test_planner_phase1.py b/amplifier/planner/tests/test_planner_phase1.py new file mode 100644 index 00000000..8c362e8c --- /dev/null +++ b/amplifier/planner/tests/test_planner_phase1.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +""" +Manual test script for Super-Planner Phase 1 +Run this script to verify the basic functionality works correctly. +""" + +import sys +import uuid +from pathlib import Path + +from amplifier.planner import Project +from amplifier.planner import Task +from amplifier.planner import TaskState +from amplifier.planner import load_project +from amplifier.planner import save_project + + +def test_basic_functionality(): + """Test basic task and project creation""" + print("πŸ§ͺ Testing Basic Functionality...") + + # Create a project + project = Project(id=str(uuid.uuid4()), name="Test Blog Project") + + print(f"βœ… Created project: {project.name}") + print(f" ID: {project.id}") + + # Create some tasks + setup_task = Task(id="setup", title="Setup Project", description="Initialize Django blog project") + + models_task = Task( + id="models", + title="Create Models", + description="Define User, Post, Comment models", + depends_on=["setup"], # Depends on setup + ) + + views_task = Task( + id="views", + title="Create Views", + description="Build list, detail, create views", + depends_on=["models"], # Depends on models + ) + + # Add tasks to project + project.add_task(setup_task) + project.add_task(models_task) + project.add_task(views_task) + + print(f"βœ… Added {len(project.tasks)} tasks to project") + + return project + + +def test_hierarchy_and_dependencies(): + """Test hierarchical structure and dependency checking""" + print("\nπŸ—οΈ Testing Hierarchy and Dependencies...") + + # Create project + project = Project(id="test-hierarchy", name="Hierarchy Test") + + # Create parent task + backend = Task(id="backend", title="Backend Development", description="API and server") + + # Create child tasks with parent + auth = Task(id="auth", title="Authentication", parent_id="backend") + api = Task(id="api", title="REST API", parent_id="backend", depends_on=["auth"]) + + # Create independent task + frontend = Task(id="frontend", title="Frontend", depends_on=["api"]) + + # Add all tasks + for task in [backend, auth, api, frontend]: + project.add_task(task) + + # Test hierarchy navigation + roots = project.get_roots() + print(f"βœ… Root tasks: {[t.title for t in roots]}") + + children = project.get_children("backend") + print(f"βœ… Backend children: {[t.title for t in children]}") + + # Test dependency checking + completed = set() # No tasks completed yet + + can_start_auth = project.tasks["auth"].can_start(completed) + can_start_api = project.tasks["api"].can_start(completed) + can_start_frontend = project.tasks["frontend"].can_start(completed) + + print(f"βœ… Can start auth (no deps): {can_start_auth}") + print(f"βœ… Can start api (needs auth): {can_start_api}") + print(f"βœ… Can start frontend (needs api): {can_start_frontend}") + + # Complete auth, test again + completed.add("auth") + can_start_api_after = project.tasks["api"].can_start(completed) + print(f"βœ… Can start api after auth done: {can_start_api_after}") + + return project + + +def test_state_management(): + """Test task state transitions""" + print("\nπŸ”„ Testing State Management...") + + task = Task(id="test-state", title="State Test", description="Testing states") + + print(f"βœ… Initial state: {task.state}") + + # Manually change state (Phase 1 doesn't have automatic transitions) + task.state = TaskState.IN_PROGRESS + print(f"βœ… Updated to: {task.state}") + + task.state = TaskState.COMPLETED + print(f"βœ… Updated to: {task.state}") + + # Test state enum values + print(f"βœ… Available states: {[state.value for state in TaskState]}") + + return task + + +def test_persistence(): + """Test save and load functionality""" + print("\nπŸ’Ύ Testing Persistence...") + + # Create test project + original_project = Project(id="persistence-test", name="Persistence Test") + + task1 = Task(id="t1", title="Task 1", description="First task") + task1.state = TaskState.COMPLETED + + task2 = Task(id="t2", title="Task 2", depends_on=["t1"]) + task2.assigned_to = "test-user" + + original_project.add_task(task1) + original_project.add_task(task2) + + print(f"βœ… Created project with {len(original_project.tasks)} tasks") + + # Save project + try: + save_project(original_project) + print("βœ… Project saved successfully") + except Exception as e: + print(f"❌ Save failed: {e}") + return None + + # Load project + try: + loaded_project = load_project("persistence-test") + if loaded_project is None: + print("❌ Load returned None") + return None + + print("βœ… Project loaded successfully") + print(f" Name: {loaded_project.name}") + print(f" Tasks: {len(loaded_project.tasks)}") + + # Verify task details preserved + loaded_t1 = loaded_project.tasks["t1"] + loaded_t2 = loaded_project.tasks["t2"] + + print(f" Task 1 state: {loaded_t1.state}") + print(f" Task 2 assigned to: {loaded_t2.assigned_to}") + print(f" Task 2 depends on: {loaded_t2.depends_on}") + + # Verify data integrity + assert loaded_t1.state == TaskState.COMPLETED + assert loaded_t2.assigned_to == "test-user" + assert loaded_t2.depends_on == ["t1"] + + print("βœ… All data preserved correctly") + + return loaded_project + + except Exception as e: + print(f"❌ Load failed: {e}") + return None + + +def test_complete_workflow(): + """Test complete project workflow""" + print("\nπŸš€ Testing Complete Workflow...") + + # Create Django blog project + project = Project(id="django-blog", name="Django Blog") + + # Define task hierarchy + tasks = [ + Task("setup", "Project Setup", "Create Django project and configure settings"), + Task("models", "Database Models", "Create User, Post, Comment models", depends_on=["setup"]), + Task("admin", "Admin Interface", "Configure Django admin", depends_on=["models"]), + Task("views", "Views", "Create list, detail, create views", depends_on=["models"]), + Task("templates", "Templates", "Design HTML templates", depends_on=["views"]), + Task("urls", "URL Configuration", "Set up routing", depends_on=["views"]), + Task("tests", "Tests", "Write unit tests", depends_on=["models", "views"]), + Task("deploy", "Deployment", "Deploy to production", depends_on=["templates", "urls", "tests"]), + ] + + # Add all tasks + for task in tasks: + project.add_task(task) + + print(f"βœ… Created project with {len(tasks)} tasks") + + # Save project + save_project(project) + print("βœ… Project saved") + + # Simulate workflow: complete tasks in dependency order + completed_tasks = set() + + def find_ready_tasks(): + ready = [] + for task in project.tasks.values(): + if task.state == TaskState.PENDING and task.can_start(completed_tasks): + ready.append(task) + return ready + + step = 1 + while completed_tasks != set(project.tasks.keys()): + ready_tasks = find_ready_tasks() + + if not ready_tasks: + break + + # "Complete" the first ready task + next_task = ready_tasks[0] + next_task.state = TaskState.COMPLETED + completed_tasks.add(next_task.id) + + print(f" Step {step}: Completed '{next_task.title}'") + step += 1 + + # Save final state + save_project(project) + print("βœ… Workflow simulation completed") + + return project + + +def show_data_files(): + """Show created data files""" + print("\nπŸ“ Created Data Files:") + + data_dir = Path("data/planner/projects") + if data_dir.exists(): + for json_file in data_dir.glob("*.json"): + print(f" πŸ“„ {json_file}") + # Show first few lines + try: + with open(json_file) as f: + content = f.read() + lines = content.split("\n")[:5] + preview = "\n".join(lines) + if len(content.split("\n")) > 5: + preview += "\n ..." + print(f" Preview:\n {preview.replace(chr(10), chr(10) + ' ')}") + except Exception: + pass + else: + print(" No data directory found") + + +def main(): + """Run all tests""" + print("🎯 Super-Planner Phase 1 Manual Testing") + print("=" * 50) + + try: + # Test basic functionality + test_basic_functionality() + + # Test hierarchy + test_hierarchy_and_dependencies() + + # Test states + test_state_management() + + # Test persistence + test_persistence() + + # Test complete workflow + test_complete_workflow() + + # Show files created + show_data_files() + + print("\nπŸŽ‰ ALL TESTS COMPLETED SUCCESSFULLY!") + print("\nWhat was tested:") + print("βœ… Task and Project creation") + print("βœ… Hierarchical structure (parents/children)") + print("βœ… Dependency management") + print("βœ… State transitions") + print("βœ… File persistence (save/load)") + print("βœ… Complete project workflow") + + print("\nπŸ’Ύ Data stored in: data/planner/projects/") + print(" You can examine the JSON files to see the structure") + + return True + + except Exception as e: + print(f"\n❌ TEST FAILED: {e}") + import traceback + + traceback.print_exc() + return False + + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) diff --git a/amplifier/planner/working_plan.md b/amplifier/planner/working_plan.md new file mode 100644 index 00000000..230d7837 --- /dev/null +++ b/amplifier/planner/working_plan.md @@ -0,0 +1,321 @@ +# Super-Planner Implementation Working Plan + +## Project Context + +**Branch**: `feature/super-planner` (currently checked out) +**Location**: `amplifier/planner/` (core library component, not scenarios) +**Philosophy**: Amplifier's "bricks and studs" modular design with ruthless simplicity + +### Problem Statement +Build a text and file-based system within amplifier that manages large projects with multiple subtasks, supporting both multiple AI agents and humans working concurrently. The system breaks down tasks recursively, manages state (not started, assigned, in progress, completed), and coordinates strategic task assignment with sub-agent spawning. + +### Key Requirements +- **Two Modes**: Planning (recursive AI task breakdown) and Working (strategic assignment + agent spawning) +- **File-Based Persistence**: All data in JSON files with git integration +- **Multi-Agent Coordination**: Multiple amplifier instances working together via Task tool +- **Strategic Assignment**: Analyze complete project plan for optimal task ordering +- **Amplifier Integration**: Uses same LLM calls, tools, and patterns as existing amplifier + +### Architecture Overview +``` +amplifier/planner/ +β”œβ”€β”€ core/ # Task, Project models, business logic +β”œβ”€β”€ modes/ # PlanningMode, WorkingMode implementations +β”œβ”€β”€ persistence/ # File storage + git integration +β”œβ”€β”€ orchestrator/ # Agent spawning + coordination +β”œβ”€β”€ session/ # LLM integration via CCSDK toolkit +└── tests/ # Golden test suite (28 scenarios) +``` + +### Integration Points +- **Task Tool**: For spawning sub-agents (`Task(subagent_type="zen-architect", prompt="...")`) +- **CCSDK Toolkit**: `SessionManager`, defensive utilities, `parse_llm_json`, `retry_with_feedback` +- **Git**: Auto-commit, recovery points, distributed coordination +- **Makefile**: CLI commands (`make planner-create`, `make planner-plan`, etc.) + +### Key Design Decisions Made +1. **Core library in amplifier/** (not scenarios/) - infrastructure component +2. **File-based storage** with git (not database) - follows amplifier patterns +3. **JSON with sorted keys** - git-friendly diffs +4. **Per-project git repos** - isolation and parallel development +5. **Optimistic locking** - version numbers prevent conflicts +6. **Agent spawning via Task tool** - reuses existing amplifier capability +7. **Three-phase implementation** - Foundation β†’ Intelligence β†’ Coordination + +### Testing Strategy +- **Spec and Test Forward**: Golden tests define behavior before implementation +- **28 Test Scenarios**: End-to-end workflows, multi-agent coordination, failure recovery +- **60-30-10 Pyramid**: Unit (state transitions) β†’ Integration (mode switching) β†’ E2E (real projects) +- **Sample Projects**: Simple (8 tasks), Complex (22 tasks), Enterprise (1000+ tasks) +- **Performance Targets**: 1000 tasks <30s load, 20 concurrent agents, <100ms state transitions + +## Implementation Tasks + +### Phase 1: Foundation (Core + Persistence) +**Priority**: P0 - Essential foundation +**Estimated Time**: 8-12 hours +**Dependencies**: None + +#### Core Module (`amplifier/planner/core/`) +- [ ] **models.py** - Complete Task, Project, TaskState data models + - Task model with 20+ fields (id, title, description, state, dependencies, metadata, etc.) + - Project model with goals, constraints, success_criteria + - TaskState enum (NOT_STARTED, ASSIGNED, IN_PROGRESS, COMPLETED, BLOCKED, CANCELLED) + - JSON serialization with sorted keys for git-friendly diffs + - Validation methods and constraints + +- [ ] **task_manager.py** - Task lifecycle management + - State transition validation with guard conditions + - Dependency graph operations (add, remove, validate) + - Cycle detection algorithms + - Task hierarchy navigation (parents, children, siblings) + +- [ ] **project_manager.py** - Project-level operations + - Project creation and initialization + - Task tree operations (get_task_tree, get_ready_tasks) + - Project statistics and health metrics + - Bulk operations on task collections + +#### Persistence Module (`amplifier/planner/persistence/`) +- [ ] **storage.py** - File-based storage with defensive I/O + - ProjectStorage class with save/load operations + - Incremental task saves (critical for multi-agent coordination) + - Use `amplifier.ccsdk_toolkit.defensive` utilities for cloud sync resilience + - File structure: `projects/{project_id}/project.json`, `tasks/{task_id}.json` + - Assignment indexes for strategic queries + +- [ ] **git_sync.py** - Git integration for persistence + - GitPersistence class with auto-commit functionality + - Semantic commit messages from operation batches + - Recovery point creation and restoration + - Per-project repository management + - Conflict detection and resolution + +#### Foundation Tests +- [ ] **test_models.py** - Data model validation + - Task creation, validation, serialization + - Project initialization and task association + - State transition validation (all valid/invalid combinations) + - Dependency cycle detection edge cases + +- [ ] **test_storage.py** - File persistence validation + - Save/load operations with various data scenarios + - Concurrent access simulation + - File corruption recovery + - Cloud sync error simulation (OneDrive/Dropbox issues) + +- [ ] **test_git_integration.py** - Git workflow validation + - Auto-commit functionality + - Recovery point creation/restoration + - Conflict resolution scenarios + - Multi-repository coordination + +### Phase 2: Intelligence (Session + Modes) +**Priority**: P0 - Core AI functionality +**Estimated Time**: 10-15 hours +**Dependencies**: Phase 1 complete + +#### Session Module (`amplifier/planner/session/`) +- [ ] **planner_session.py** - LLM integration wrapper + - PlannerSession class wrapping CCSDK SessionManager + - Task decomposition prompts and response parsing + - Strategic task analysis (complexity, tool requirements, human vs AI) + - Integration with `parse_llm_json` for reliable response handling + - Retry logic with `retry_with_feedback` for API failures + +#### Modes Module (`amplifier/planner/modes/`) +- [ ] **base.py** - Mode interface and switching logic + - PlannerMode abstract base class + - Mode switching protocols (planning ↔ working) + - Context preservation across mode changes + - State validation for mode transitions + +- [ ] **planning.py** - AI-driven task decomposition + - PlanningMode implementation with recursive breakdown + - LLM prompts for project analysis and task creation + - Task depth limits and complexity assessment + - Integration with existing project structure + - Incremental saves after each decomposition + +- [ ] **working.py** - Strategic execution mode + - WorkingMode implementation with agent coordination + - Ready task identification (satisfied dependencies) + - Agent capability matching and task assignment + - Progress monitoring and state updates + - New task scheduling as work completes + +#### Intelligence Tests +- [ ] **test_session.py** - LLM integration validation + - Mock LLM responses for deterministic testing + - Error handling for API failures, timeouts, rate limits + - Response parsing validation (malformed JSON, unexpected formats) + - Retry logic verification + +- [ ] **test_modes.py** - Mode operation validation + - Planning mode task decomposition scenarios + - Working mode task assignment logic + - Mode switching with state preservation + - Error recovery during mode operations + +### Phase 3: Coordination (Orchestrator) +**Priority**: P0 - Multi-agent capability +**Estimated Time**: 12-18 hours +**Dependencies**: Phase 2 complete + +#### Orchestrator Module (`amplifier/planner/orchestrator/`) +- [ ] **coordinator.py** - Agent spawning and lifecycle management + - AgentCoordinator class for managing multiple amplifier agents + - Integration with amplifier's Task tool for agent spawning + - Agent status tracking (starting, running, completed, failed) + - Resource management (concurrent agent limits) + - Agent cleanup and failure recovery + +- [ ] **task_assigner.py** - Strategic task assignment + - TaskAssigner class for intelligent task matching + - Task analysis (complexity, required tools, urgency) + - Agent capability matching and load balancing + - Priority-based scheduling algorithms + - Dependency-aware assignment ordering + +#### API and CLI Integration +- [ ] **__init__.py** - Public API contracts ("studs") + - Main entry points: `create_planner()`, `load_planner()` + - Public classes: Task, Project, PlannerMode, etc. + - Integration protocols for other amplifier modules + - Version management and backward compatibility + +- [ ] **cli.py** - Command-line interface + - Make command integration (`planner-create`, `planner-plan`, `planner-work`) + - Project management commands (create, list, delete) + - Task operations (show, assign, complete) + - Agent coordination commands (spawn, monitor, cleanup) + +#### Coordination Tests +- [ ] **test_orchestrator.py** - Multi-agent coordination + - Agent spawning with real Task tool integration + - Concurrent task execution simulation + - Resource contention and load balancing + - Agent failure recovery scenarios + +- [ ] **test_integration.py** - Full system integration + - End-to-end workflows with real LLM calls + - Multi-agent project execution + - Git operations under concurrent load + - Performance validation at scale + +### Phase 4: Golden Test Suite Implementation +**Priority**: P1 - Quality assurance +**Estimated Time**: 6-10 hours +**Dependencies**: Phase 3 complete + +#### Golden Test Data +- [ ] **fixtures/simple_web_app.json** - Basic workflow validation + - 8-task Django blog project + - 2-3 levels of task hierarchy + - Expected completion in ~2 hours with 2-3 agents + +- [ ] **fixtures/complex_microservices.json** - Multi-agent coordination + - 22-task e-commerce platform + - 4-5 levels of task hierarchy + - Expected completion in ~1 week with 8-10 agents + +- [ ] **fixtures/enterprise_migration.json** - Scale validation + - 500-1000 tasks legacy system migration + - 5+ levels of task hierarchy + - Expected completion in ~3 months with 15-20 agents + +#### Integration Test Scenarios +- [ ] **test_golden_workflows.py** - Complete workflow validation + - Simple project end-to-end execution + - Complex project multi-agent coordination + - Enterprise project scale testing + - Performance benchmark validation + +- [ ] **test_failure_scenarios.py** - Reliability validation + - Network partition during agent coordination + - File corruption and recovery + - Agent crashes mid-execution + - Git conflicts during concurrent updates + - LLM API failures and recovery + +### Phase 5: Documentation and Deployment +**Priority**: P2 - Production readiness +**Estimated Time**: 4-6 hours +**Dependencies**: Phase 4 complete + +#### Documentation +- [ ] **README.md** - Main module documentation + - Quick start guide and usage examples + - Architecture overview and philosophy + - API reference and integration points + - Troubleshooting and FAQ + +- [ ] **CONTRACTS.md** - API contract specifications + - Public API stability guarantees + - Integration protocols documentation + - Extension points for customization + - Version management strategy + +#### Production Integration +- [ ] **Makefile integration** - Add planner commands to main Makefile + - `make planner-create PROJECT="name"` - Create new project + - `make planner-plan PROJECT_ID="uuid"` - Run planning mode + - `make planner-work PROJECT_ID="uuid"` - Run working mode + - `make planner-status PROJECT_ID="uuid"` - Show project status + +- [ ] **VSCode integration** - Workspace configuration + - Exclude planner data files from search + - Git integration for planner repositories + - Task highlighting and navigation + +## Current Status + +**Completed Design Work:** +- βœ… Complete architecture design with all module specifications +- βœ… Comprehensive data model design with file structure +- βœ… Coordination protocols for multi-agent workflow +- βœ… Sub-agent spawning mechanisms via Task tool +- βœ… Git integration and persistence strategy +- βœ… Golden test specifications (28 scenarios) +- βœ… API contracts and integration points + +**Ready to Begin Implementation:** +- Phase 1 specifications are complete and detailed +- Test requirements are clearly defined +- Integration points with existing amplifier code are mapped +- File structure and data formats are specified +- Git workflow is designed and validated + +**Implementation Priority:** +1. Start with Phase 1 (Foundation) - most critical for system stability +2. Implement golden tests alongside each module for validation +3. Use existing amplifier patterns (ccsdk_toolkit, defensive utilities) +4. Follow "bricks and studs" philosophy - stable contracts, regenerable internals + +## Key Implementation Notes + +### Critical Integration Points +- **CCSDK Toolkit**: Use `SessionManager`, `parse_llm_json`, `retry_with_feedback` +- **Defensive File I/O**: Handle OneDrive/cloud sync issues with retry patterns +- **Task Tool**: Agent spawning via `Task(subagent_type="...", prompt="...")` +- **Git Workflow**: Auto-commit with semantic messages, recovery points + +### Performance Considerations +- **Incremental saves**: Save after every task state change +- **Selective loading**: Only load tasks needed for current operation +- **Batch operations**: Group related changes into single git commits +- **Caching**: Task data caching with file modification time checks + +### Error Handling Patterns +- **Optimistic locking**: Version numbers prevent concurrent modification conflicts +- **Retry with backoff**: File I/O, LLM API calls, git operations +- **Circuit breakers**: Graceful degradation when components fail +- **Recovery points**: Git tags for rollback capability + +### Testing Strategy +- **Test-first development**: Implement golden tests before each module +- **Real integration**: Use actual Task tool, LLM APIs, git operations +- **Failure simulation**: Network issues, file corruption, agent crashes +- **Performance validation**: Scale testing with 1000+ tasks, 20 agents + +This working plan captures the complete context needed to implement the Super-Planner system. Future sessions can pick up from any phase and understand the full scope, design decisions, and implementation requirements. \ No newline at end of file diff --git a/data/planner/projects/demo.json b/data/planner/projects/demo.json new file mode 100644 index 00000000..e08f351c --- /dev/null +++ b/data/planner/projects/demo.json @@ -0,0 +1,67 @@ +{ + "id": "demo", + "name": "Demo Project", + "created_at": "2025-10-07T10:21:31.319518", + "updated_at": "2025-10-07T10:21:31.319800", + "tasks": { + "epic1": { + "id": "epic1", + "title": "Build Authentication", + "description": "Complete auth system", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T10:21:31.319543", + "updated_at": "2025-10-07T10:21:31.319545" + }, + "epic2": { + "id": "epic2", + "title": "Build Dashboard", + "description": "User dashboard", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T10:21:31.319552", + "updated_at": "2025-10-07T10:21:31.319553" + }, + "auth_design": { + "id": "auth_design", + "title": "Design Auth Flow", + "description": "", + "state": "completed", + "parent_id": "epic1", + "depends_on": [], + "assigned_to": "architect", + "created_at": "2025-10-07T10:21:31.319776", + "updated_at": "2025-10-07T10:21:31.319777" + }, + "auth_impl": { + "id": "auth_impl", + "title": "Implement Auth", + "description": "", + "state": "in_progress", + "parent_id": "epic1", + "depends_on": [ + "auth_design" + ], + "assigned_to": "backend-dev", + "created_at": "2025-10-07T10:21:31.319784", + "updated_at": "2025-10-07T10:21:31.319785" + }, + "auth_test": { + "id": "auth_test", + "title": "Test Auth", + "description": "", + "state": "pending", + "parent_id": "epic1", + "depends_on": [ + "auth_impl" + ], + "assigned_to": "qa", + "created_at": "2025-10-07T10:21:31.319791", + "updated_at": "2025-10-07T10:21:31.319791" + } + } +} \ No newline at end of file diff --git a/data/planner/projects/django-blog.json b/data/planner/projects/django-blog.json new file mode 100644 index 00000000..29fa3f5f --- /dev/null +++ b/data/planner/projects/django-blog.json @@ -0,0 +1,113 @@ +{ + "id": "django-blog", + "name": "Django Blog", + "created_at": "2025-10-07T15:49:19.974809", + "updated_at": "2025-10-07T15:49:19.974843", + "tasks": { + "setup": { + "id": "setup", + "title": "Project Setup", + "description": "Create Django project and configure settings", + "state": "completed", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974818", + "updated_at": "2025-10-07T15:49:19.974819" + }, + "models": { + "id": "models", + "title": "Database Models", + "description": "Create User, Post, Comment models", + "state": "completed", + "parent_id": null, + "depends_on": [ + "setup" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974822", + "updated_at": "2025-10-07T15:49:19.974822" + }, + "admin": { + "id": "admin", + "title": "Admin Interface", + "description": "Configure Django admin", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974824", + "updated_at": "2025-10-07T15:49:19.974825" + }, + "views": { + "id": "views", + "title": "Views", + "description": "Create list, detail, create views", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974826", + "updated_at": "2025-10-07T15:49:19.974827" + }, + "templates": { + "id": "templates", + "title": "Templates", + "description": "Design HTML templates", + "state": "completed", + "parent_id": null, + "depends_on": [ + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974828", + "updated_at": "2025-10-07T15:49:19.974829" + }, + "urls": { + "id": "urls", + "title": "URL Configuration", + "description": "Set up routing", + "state": "completed", + "parent_id": null, + "depends_on": [ + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974830", + "updated_at": "2025-10-07T15:49:19.974830" + }, + "tests": { + "id": "tests", + "title": "Tests", + "description": "Write unit tests", + "state": "completed", + "parent_id": null, + "depends_on": [ + "models", + "views" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974832", + "updated_at": "2025-10-07T15:49:19.974832" + }, + "deploy": { + "id": "deploy", + "title": "Deployment", + "description": "Deploy to production", + "state": "completed", + "parent_id": null, + "depends_on": [ + "templates", + "urls", + "tests" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.974834", + "updated_at": "2025-10-07T15:49:19.974834" + } + } +} \ No newline at end of file diff --git a/data/planner/projects/persistence-test.json b/data/planner/projects/persistence-test.json new file mode 100644 index 00000000..2d5d5e1b --- /dev/null +++ b/data/planner/projects/persistence-test.json @@ -0,0 +1,32 @@ +{ + "id": "persistence-test", + "name": "Persistence Test", + "created_at": "2025-10-07T15:49:19.863395", + "updated_at": "2025-10-07T15:49:19.863413", + "tasks": { + "t1": { + "id": "t1", + "title": "Task 1", + "description": "First task", + "state": "completed", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:49:19.863404", + "updated_at": "2025-10-07T15:49:19.863404" + }, + "t2": { + "id": "t2", + "title": "Task 2", + "description": "", + "state": "pending", + "parent_id": null, + "depends_on": [ + "t1" + ], + "assigned_to": "test-user", + "created_at": "2025-10-07T15:49:19.863408", + "updated_at": "2025-10-07T15:49:19.863408" + } + } +} \ No newline at end of file diff --git a/data/planner/projects/test_integration_project_20251007_121405.json b/data/planner/projects/test_integration_project_20251007_121405.json new file mode 100644 index 00000000..fac787e0 --- /dev/null +++ b/data/planner/projects/test_integration_project_20251007_121405.json @@ -0,0 +1,19 @@ +{ + "id": "test_integration_project_20251007_121405", + "name": "Test Integration Project", + "created_at": "2025-10-07T12:14:49.381472", + "updated_at": "2025-10-07T13:01:58.155560", + "tasks": { + "main-goal": { + "id": "main-goal", + "title": "Smart Task Decomposer CLI: Create scenarios/smart_decomposer/ - hybrid CLI tool that takes high-level goals and intelligently breaks them into specific, actionable tasks with agent assignment recommendations", + "description": "Main project goal: Smart Task Decomposer CLI: Create scenarios/smart_decomposer/ - hybrid CLI tool that takes high-level goals and intelligently breaks them into specific, actionable tasks with agent assignment recommendations", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T13:01:58.155556", + "updated_at": "2025-10-07T13:01:58.155558" + } + } +} \ No newline at end of file diff --git a/data/planner/projects/vi-rewrite.json b/data/planner/projects/vi-rewrite.json new file mode 100644 index 00000000..20345787 --- /dev/null +++ b/data/planner/projects/vi-rewrite.json @@ -0,0 +1,975 @@ +{ + "id": "vi-rewrite", + "name": "Vi Editor Rewrite in Python", + "created_at": "2025-10-07T15:54:49.801147", + "updated_at": "2025-10-07T15:54:49.869986", + "tasks": { + "arch": { + "id": "arch", + "title": "Architecture & Design", + "description": "Define overall architecture, data structures, and interfaces", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801148", + "updated_at": "2025-10-07T15:54:49.801149" + }, + "core": { + "id": "core", + "title": "Core Editing Engine", + "description": "Text buffer management, cursor handling, basic operations", + "state": "pending", + "parent_id": null, + "depends_on": [ + "arch" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801150", + "updated_at": "2025-10-07T15:54:49.801150" + }, + "commands": { + "id": "commands", + "title": "Command System", + "description": "Vi command parsing, execution, and modal interface", + "state": "pending", + "parent_id": null, + "depends_on": [ + "arch" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801151", + "updated_at": "2025-10-07T15:54:49.801151" + }, + "ui": { + "id": "ui", + "title": "User Interface System", + "description": "Terminal handling, screen rendering, input processing", + "state": "pending", + "parent_id": null, + "depends_on": [ + "arch" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801152", + "updated_at": "2025-10-07T15:54:49.801152" + }, + "filesystem": { + "id": "filesystem", + "title": "File Operations", + "description": "File I/O, backup, recovery, file type detection", + "state": "pending", + "parent_id": null, + "depends_on": [ + "arch" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801152", + "updated_at": "2025-10-07T15:54:49.801152" + }, + "arch-analysis": { + "id": "arch-analysis", + "title": "Requirements Analysis", + "description": "Analyze vi behavior, commands, edge cases", + "state": "pending", + "parent_id": "arch", + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801154", + "updated_at": "2025-10-07T15:54:49.801154" + }, + "arch-design": { + "id": "arch-design", + "title": "System Design", + "description": "Design core data structures and interfaces", + "state": "pending", + "parent_id": "arch", + "depends_on": [ + "arch-analysis" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801155", + "updated_at": "2025-10-07T15:54:49.801155" + }, + "arch-patterns": { + "id": "arch-patterns", + "title": "Design Patterns", + "description": "Define patterns for extensibility", + "state": "pending", + "parent_id": "arch", + "depends_on": [ + "arch-design" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801156", + "updated_at": "2025-10-07T15:54:49.801156" + }, + "arch-docs": { + "id": "arch-docs", + "title": "Architecture Documentation", + "description": "Document architecture decisions", + "state": "pending", + "parent_id": "arch", + "depends_on": [ + "arch-patterns" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801157", + "updated_at": "2025-10-07T15:54:49.801157" + }, + "core-buffer": { + "id": "core-buffer", + "title": "Text Buffer", + "description": "Efficient text storage with undo/redo", + "state": "pending", + "parent_id": "core", + "depends_on": [ + "core" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801158", + "updated_at": "2025-10-07T15:54:49.801158" + }, + "core-cursor": { + "id": "core-cursor", + "title": "Cursor Management", + "description": "Cursor positioning, movement, selection", + "state": "pending", + "parent_id": "core", + "depends_on": [ + "core-buffer" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801159", + "updated_at": "2025-10-07T15:54:49.801159" + }, + "core-operations": { + "id": "core-operations", + "title": "Basic Operations", + "description": "Insert, delete, replace operations", + "state": "pending", + "parent_id": "core", + "depends_on": [ + "core-cursor" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801160", + "updated_at": "2025-10-07T15:54:49.801160" + }, + "core-marks": { + "id": "core-marks", + "title": "Marks and Registers", + "description": "Named marks, registers, clipboard", + "state": "pending", + "parent_id": "core", + "depends_on": [ + "core-operations" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801160", + "updated_at": "2025-10-07T15:54:49.801161" + }, + "core-search": { + "id": "core-search", + "title": "Search Engine", + "description": "Pattern matching, regex, search/replace", + "state": "pending", + "parent_id": "core", + "depends_on": [ + "core-operations" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801161", + "updated_at": "2025-10-07T15:54:49.801161" + }, + "buffer-gap": { + "id": "buffer-gap", + "title": "Gap Buffer Implementation", + "description": "Efficient gap buffer with automatic expansion", + "state": "pending", + "parent_id": "core-buffer", + "depends_on": [ + "core-buffer" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801162", + "updated_at": "2025-10-07T15:54:49.801162" + }, + "buffer-lines": { + "id": "buffer-lines", + "title": "Line Management", + "description": "Line indexing, wrapping, virtual lines", + "state": "pending", + "parent_id": "core-buffer", + "depends_on": [ + "buffer-gap" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801163", + "updated_at": "2025-10-07T15:54:49.801163" + }, + "buffer-encoding": { + "id": "buffer-encoding", + "title": "Text Encoding", + "description": "UTF-8, line endings, BOM handling", + "state": "pending", + "parent_id": "core-buffer", + "depends_on": [ + "buffer-lines" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801164", + "updated_at": "2025-10-07T15:54:49.801164" + }, + "buffer-undo": { + "id": "buffer-undo", + "title": "Undo System", + "description": "Efficient undo/redo with branching", + "state": "pending", + "parent_id": "core-buffer", + "depends_on": [ + "buffer-encoding" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801165", + "updated_at": "2025-10-07T15:54:49.801165" + }, + "buffer-diff": { + "id": "buffer-diff", + "title": "Change Tracking", + "description": "Track changes for diff, backup", + "state": "pending", + "parent_id": "core-buffer", + "depends_on": [ + "buffer-undo" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801166", + "updated_at": "2025-10-07T15:54:49.801166" + }, + "cmd-parser": { + "id": "cmd-parser", + "title": "Command Parser", + "description": "Parse vi commands with error handling", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "commands" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801167", + "updated_at": "2025-10-07T15:54:49.801167" + }, + "cmd-modes": { + "id": "cmd-modes", + "title": "Modal Interface", + "description": "Normal, insert, visual, command modes", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "cmd-parser" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801167", + "updated_at": "2025-10-07T15:54:49.801168" + }, + "cmd-movement": { + "id": "cmd-movement", + "title": "Movement Commands", + "description": "h,j,k,l, w,e,b, $,^, G, etc.", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "cmd-modes" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801168", + "updated_at": "2025-10-07T15:54:49.801168" + }, + "cmd-editing": { + "id": "cmd-editing", + "title": "Editing Commands", + "description": "i,a,o, d,c,y, p, u, ., etc.", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "cmd-movement" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801169", + "updated_at": "2025-10-07T15:54:49.801169" + }, + "cmd-visual": { + "id": "cmd-visual", + "title": "Visual Mode", + "description": "Visual selection, visual block", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "cmd-editing" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801170", + "updated_at": "2025-10-07T15:54:49.801170" + }, + "cmd-ex": { + "id": "cmd-ex", + "title": "Ex Commands", + "description": ":w, :q, :s, :!, etc.", + "state": "pending", + "parent_id": "commands", + "depends_on": [ + "cmd-visual" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801171", + "updated_at": "2025-10-07T15:54:49.801171" + }, + "move-char": { + "id": "move-char", + "title": "Character Movement", + "description": "h,l,space,backspace movement", + "state": "pending", + "parent_id": "cmd-movement", + "depends_on": [ + "cmd-movement" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801172", + "updated_at": "2025-10-07T15:54:49.801172" + }, + "move-word": { + "id": "move-word", + "title": "Word Movement", + "description": "w,e,b,W,E,B word boundaries", + "state": "pending", + "parent_id": "cmd-movement", + "depends_on": [ + "move-char" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801173", + "updated_at": "2025-10-07T15:54:49.801173" + }, + "move-line": { + "id": "move-line", + "title": "Line Movement", + "description": "j,k,$,^,0,+,- movements", + "state": "pending", + "parent_id": "cmd-movement", + "depends_on": [ + "move-word" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801174", + "updated_at": "2025-10-07T15:54:49.801174" + }, + "move-search": { + "id": "move-search", + "title": "Search Movement", + "description": "f,F,t,T,;,, and /,? searches", + "state": "pending", + "parent_id": "cmd-movement", + "depends_on": [ + "move-line" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801174", + "updated_at": "2025-10-07T15:54:49.801175" + }, + "move-jump": { + "id": "move-jump", + "title": "Jump Commands", + "description": "G,gg,H,M,L,ctrl-f,ctrl-b", + "state": "pending", + "parent_id": "cmd-movement", + "depends_on": [ + "move-search" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801175", + "updated_at": "2025-10-07T15:54:49.801175" + }, + "ui-terminal": { + "id": "ui-terminal", + "title": "Terminal Interface", + "description": "Raw terminal control, escape sequences", + "state": "pending", + "parent_id": "ui", + "depends_on": [ + "ui" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801180", + "updated_at": "2025-10-07T15:54:49.801180" + }, + "ui-screen": { + "id": "ui-screen", + "title": "Screen Management", + "description": "Screen buffer, scrolling, refresh", + "state": "pending", + "parent_id": "ui", + "depends_on": [ + "ui-terminal" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801181", + "updated_at": "2025-10-07T15:54:49.801181" + }, + "ui-input": { + "id": "ui-input", + "title": "Input Processing", + "description": "Key mapping, special keys, timing", + "state": "pending", + "parent_id": "ui", + "depends_on": [ + "ui-screen" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801182", + "updated_at": "2025-10-07T15:54:49.801182" + }, + "ui-rendering": { + "id": "ui-rendering", + "title": "Text Rendering", + "description": "Syntax highlighting, line numbers", + "state": "pending", + "parent_id": "ui", + "depends_on": [ + "ui-input" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801183", + "updated_at": "2025-10-07T15:54:49.801183" + }, + "ui-status": { + "id": "ui-status", + "title": "Status Line", + "description": "Mode indicator, position, filename", + "state": "pending", + "parent_id": "ui", + "depends_on": [ + "ui-rendering" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801184", + "updated_at": "2025-10-07T15:54:49.801184" + }, + "file-io": { + "id": "file-io", + "title": "Basic File I/O", + "description": "Read/write files, error handling", + "state": "pending", + "parent_id": "filesystem", + "depends_on": [ + "filesystem" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801185", + "updated_at": "2025-10-07T15:54:49.801185" + }, + "file-backup": { + "id": "file-backup", + "title": "Backup System", + "description": "Swap files, backup files, recovery", + "state": "pending", + "parent_id": "filesystem", + "depends_on": [ + "file-io" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801200", + "updated_at": "2025-10-07T15:54:49.801201" + }, + "file-detection": { + "id": "file-detection", + "title": "File Type Detection", + "description": "Syntax detection, file associations", + "state": "pending", + "parent_id": "filesystem", + "depends_on": [ + "file-backup" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801202", + "updated_at": "2025-10-07T15:54:49.801202" + }, + "file-encoding": { + "id": "file-encoding", + "title": "Encoding Detection", + "description": "Auto-detect and handle encodings", + "state": "pending", + "parent_id": "filesystem", + "depends_on": [ + "file-detection" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801203", + "updated_at": "2025-10-07T15:54:49.801203" + }, + "integration-basic": { + "id": "integration-basic", + "title": "Basic Integration", + "description": "Integrate core editing with UI", + "state": "pending", + "parent_id": null, + "depends_on": [ + "core-operations", + "cmd-editing", + "ui-screen" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801204", + "updated_at": "2025-10-07T15:54:49.801205" + }, + "integration-advanced": { + "id": "integration-advanced", + "title": "Advanced Integration", + "description": "Integrate search, ex commands", + "state": "pending", + "parent_id": null, + "depends_on": [ + "integration-basic", + "core-search", + "cmd-ex" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801206", + "updated_at": "2025-10-07T15:54:49.801206" + }, + "integration-files": { + "id": "integration-files", + "title": "File Integration", + "description": "Integrate file operations", + "state": "pending", + "parent_id": null, + "depends_on": [ + "integration-advanced", + "file-encoding" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801207", + "updated_at": "2025-10-07T15:54:49.801207" + }, + "test-unit": { + "id": "test-unit", + "title": "Unit Tests", + "description": "Unit tests for all components", + "state": "pending", + "parent_id": null, + "depends_on": [ + "buffer-diff", + "move-jump", + "ui-status" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801208", + "updated_at": "2025-10-07T15:54:49.801208" + }, + "test-integration": { + "id": "test-integration", + "title": "Integration Tests", + "description": "Integration tests across components", + "state": "pending", + "parent_id": null, + "depends_on": [ + "test-unit", + "integration-files" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801209", + "updated_at": "2025-10-07T15:54:49.801209" + }, + "test-compatibility": { + "id": "test-compatibility", + "title": "Vi Compatibility Tests", + "description": "Test compatibility with real vi", + "state": "pending", + "parent_id": null, + "depends_on": [ + "test-integration" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801210", + "updated_at": "2025-10-07T15:54:49.801210" + }, + "test-performance": { + "id": "test-performance", + "title": "Performance Tests", + "description": "Large file performance, memory usage", + "state": "pending", + "parent_id": null, + "depends_on": [ + "test-compatibility" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801210", + "updated_at": "2025-10-07T15:54:49.801211" + }, + "test-edge-cases": { + "id": "test-edge-cases", + "title": "Edge Case Tests", + "description": "Binary files, huge files, corner cases", + "state": "pending", + "parent_id": null, + "depends_on": [ + "test-performance" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801211", + "updated_at": "2025-10-07T15:54:49.801211" + }, + "polish-docs": { + "id": "polish-docs", + "title": "Documentation", + "description": "User manual, developer docs", + "state": "pending", + "parent_id": null, + "depends_on": [ + "test-edge-cases" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801212", + "updated_at": "2025-10-07T15:54:49.801213" + }, + "polish-package": { + "id": "polish-package", + "title": "Packaging", + "description": "Setup.py, distribution, installation", + "state": "pending", + "parent_id": null, + "depends_on": [ + "polish-docs" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801213", + "updated_at": "2025-10-07T15:54:49.801214" + }, + "polish-ci": { + "id": "polish-ci", + "title": "CI/CD Setup", + "description": "GitHub actions, automated testing", + "state": "pending", + "parent_id": null, + "depends_on": [ + "polish-package" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801215", + "updated_at": "2025-10-07T15:54:49.801215" + }, + "release-beta": { + "id": "release-beta", + "title": "Beta Release", + "description": "Initial beta release", + "state": "pending", + "parent_id": null, + "depends_on": [ + "polish-ci" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801215", + "updated_at": "2025-10-07T15:54:49.801216" + }, + "release-stable": { + "id": "release-stable", + "title": "Stable Release", + "description": "1.0 stable release", + "state": "pending", + "parent_id": null, + "depends_on": [ + "release-beta" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801216", + "updated_at": "2025-10-07T15:54:49.801217" + }, + "stress-00": { + "id": "stress-00", + "title": "Stress Test Task 0", + "description": "Random stress test task with 2 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "ui-input", + "move-search" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801284", + "updated_at": "2025-10-07T15:54:49.801284" + }, + "stress-01": { + "id": "stress-01", + "title": "Stress Test Task 1", + "description": "Random stress test task with 2 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "core", + "polish-ci" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801295", + "updated_at": "2025-10-07T15:54:49.801295" + }, + "stress-02": { + "id": "stress-02", + "title": "Stress Test Task 2", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801300", + "updated_at": "2025-10-07T15:54:49.801300" + }, + "stress-03": { + "id": "stress-03", + "title": "Stress Test Task 3", + "description": "Random stress test task with 2 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "move-search", + "buffer-gap" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801305", + "updated_at": "2025-10-07T15:54:49.801306" + }, + "stress-04": { + "id": "stress-04", + "title": "Stress Test Task 4", + "description": "Random stress test task with 2 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "move-search", + "polish-docs" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801310", + "updated_at": "2025-10-07T15:54:49.801310" + }, + "stress-05": { + "id": "stress-05", + "title": "Stress Test Task 5", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801313", + "updated_at": "2025-10-07T15:54:49.801313" + }, + "stress-06": { + "id": "stress-06", + "title": "Stress Test Task 6", + "description": "Random stress test task with 1 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "ui-status" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801316", + "updated_at": "2025-10-07T15:54:49.801316" + }, + "stress-07": { + "id": "stress-07", + "title": "Stress Test Task 7", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "core-cursor", + "move-line", + "release-beta" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801320", + "updated_at": "2025-10-07T15:54:49.801320" + }, + "stress-08": { + "id": "stress-08", + "title": "Stress Test Task 8", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801326", + "updated_at": "2025-10-07T15:54:49.801326" + }, + "stress-09": { + "id": "stress-09", + "title": "Stress Test Task 9", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801329", + "updated_at": "2025-10-07T15:54:49.801329" + }, + "stress-10": { + "id": "stress-10", + "title": "Stress Test Task 10", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "polish-docs", + "core-search", + "cmd-modes" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801333", + "updated_at": "2025-10-07T15:54:49.801333" + }, + "stress-11": { + "id": "stress-11", + "title": "Stress Test Task 11", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801336", + "updated_at": "2025-10-07T15:54:49.801336" + }, + "stress-12": { + "id": "stress-12", + "title": "Stress Test Task 12", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "core", + "ui", + "stress-02" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801340", + "updated_at": "2025-10-07T15:54:49.801340" + }, + "stress-13": { + "id": "stress-13", + "title": "Stress Test Task 13", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "move-word", + "move-line", + "file-io" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801344", + "updated_at": "2025-10-07T15:54:49.801344" + }, + "stress-14": { + "id": "stress-14", + "title": "Stress Test Task 14", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "ui-input", + "filesystem", + "ui-status" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801349", + "updated_at": "2025-10-07T15:54:49.801349" + }, + "stress-15": { + "id": "stress-15", + "title": "Stress Test Task 15", + "description": "Random stress test task with 1 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "buffer-gap" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801353", + "updated_at": "2025-10-07T15:54:49.801353" + }, + "stress-16": { + "id": "stress-16", + "title": "Stress Test Task 16", + "description": "Random stress test task with 1 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "move-jump" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801356", + "updated_at": "2025-10-07T15:54:49.801357" + }, + "stress-17": { + "id": "stress-17", + "title": "Stress Test Task 17", + "description": "Random stress test task with 0 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801359", + "updated_at": "2025-10-07T15:54:49.801359" + }, + "stress-18": { + "id": "stress-18", + "title": "Stress Test Task 18", + "description": "Random stress test task with 2 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "stress-15", + "integration-files" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801363", + "updated_at": "2025-10-07T15:54:49.801363" + }, + "stress-19": { + "id": "stress-19", + "title": "Stress Test Task 19", + "description": "Random stress test task with 3 dependencies", + "state": "pending", + "parent_id": null, + "depends_on": [ + "arch-analysis", + "file-backup", + "stress-14" + ], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.801367", + "updated_at": "2025-10-07T15:54:49.801367" + }, + "extreme-data": { + "id": "extreme-data", + "title": "Task with extreme data", + "description": "Description with every Unicode character: !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", + "state": "pending", + "parent_id": null, + "depends_on": [], + "assigned_to": "user with πŸ”₯Γ©mojisπŸ’» and spΓ«ciΓ₯l chars", + "created_at": "2025-10-07T15:54:49.802479", + "updated_at": "2025-10-07T15:54:49.802480" + }, + "state-test": { + "id": "state-test", + "title": "State Transition Test", + "description": "", + "state": "blocked", + "parent_id": null, + "depends_on": [], + "assigned_to": null, + "created_at": "2025-10-07T15:54:49.869981", + "updated_at": "2025-10-07T15:54:49.869984" + } + } +} \ No newline at end of file diff --git a/scenarios/project_planner/README.md b/scenarios/project_planner/README.md new file mode 100644 index 00000000..0d9ea2d9 --- /dev/null +++ b/scenarios/project_planner/README.md @@ -0,0 +1,140 @@ +# Project Planner: AI-Driven Multi-Agent Project Orchestration + +**Turn complex projects into coordinated task execution across specialized AI agents.** + +## The Problem + +Managing complex projects with multiple interconnected tasks is challenging: + +- **Manual coordination** - Breaking down projects and coordinating execution takes significant time +- **Context switching** - Jumping between different types of work (coding, documentation, testing) disrupts flow +- **Agent specialization** - Different tasks need different expertise, but coordination is manual +- **State management** - Tracking progress across multiple sessions and agents is error-prone +- **Dependency complexity** - Understanding what can be done in parallel vs. sequentially requires constant mental overhead + +## The Solution + +Project Planner is a persistent AI planning system that: + +1. **Analyzes project complexity** - Uses AI to break down high-level goals into actionable tasks +2. **Builds dependency graphs** - Creates hierarchical task structures with proper dependency management +3. **Assigns specialized agents** - Maps tasks to the most appropriate AI agents based on capability +4. **Coordinates execution** - Orchestrates multi-agent workflows with proper sequencing +5. **Persists across sessions** - Maintains project state and progress through multiple amplifier invocations + +**The result**: Complex projects execute themselves through coordinated AI agents, while you focus on high-level decisions. + +## Quick Start + +**Prerequisites**: Complete the [Amplifier setup instructions](../../README.md#-step-by-step-setup) first. + +### Initialize Project Planning + +```bash +# Initialize project with planning +make project-init PROJECT_NAME="My Web App" + +# Or manually +uv run python -m scenarios.project_planner init --name "My Web App" +``` + +### Plan Project Tasks + +```bash +# AI-driven task decomposition +make project-plan + +# Or with specific goals +uv run python -m scenarios.project_planner plan --goals "Build user authentication system" +``` + +### Execute Coordinated Workflow + +```bash +# Execute tasks with multi-agent coordination +make project-execute + +# Check status and progress +make project-status +``` + +## Core Features + +### 🧠 AI Task Decomposition +- Natural language project description β†’ structured task hierarchy +- Intelligent dependency detection and management +- Automatic subtask generation for complex work + +### πŸ€– Multi-Agent Coordination +- Maps tasks to specialized agents (zen-architect, bug-hunter, test-coverage, etc.) +- Parallel execution where dependencies allow +- Context sharing between related agents + +### πŸ’Ύ Persistent State Management +- Project context survives across amplifier sessions +- Progress tracking with automatic checkpointing +- Resumable workflows after interruptions + +### πŸ“Š Intelligent Progress Monitoring +- Real-time dependency resolution +- Bottleneck identification and suggestions +- Completion estimation and milestone tracking + +## Project Structure + +``` +.amplifier/ +β”œβ”€β”€ project.json # Project configuration and metadata +└── sessions/ + └── planning_*.json # Session state for resumable workflows + +data/planner/projects/ +└── {project_id}.json # Task hierarchy and dependency graph +``` + +## Integration with Amplifier + +Project Planner integrates seamlessly with amplifier's core workflow: + +- **Auto-detection**: Amplifier automatically detects project context via `.amplifier/project.json` +- **Cross-session state**: Planning context persists across amplifier invocations +- **Agent coordination**: Uses amplifier's existing agent ecosystem for task execution +- **Zero configuration**: Works without setup once project is initialized + +## Advanced Usage + +### Custom Task Hierarchies + +```python +# Define custom task breakdown +uv run python -m scenarios.project_planner plan \ + --template custom_hierarchy.json \ + --depth 4 +``` + +### Agent Assignment Control + +```python +# Control which agents handle which tasks +uv run python -m scenarios.project_planner assign \ + --task "backend-api" \ + --agent zen-architect +``` + +### Workflow Orchestration + +```python +# Execute specific workflow patterns +uv run python -m scenarios.project_planner execute \ + --pattern parallel_branches \ + --max_agents 3 +``` + +## Philosophy + +Project Planner embodies amplifier's core principle: **"Code for structure, AI for intelligence"** + +- **Structure**: Reliable task management, dependency tracking, and state persistence +- **Intelligence**: AI-driven planning, agent coordination, and adaptive execution + +The result is a system that handles the mechanical complexity of project orchestration while letting AI agents focus on the creative and technical work they do best. \ No newline at end of file diff --git a/scenarios/project_planner/__init__.py b/scenarios/project_planner/__init__.py new file mode 100644 index 00000000..a94af456 --- /dev/null +++ b/scenarios/project_planner/__init__.py @@ -0,0 +1,5 @@ +""" +Project Planner - AI-driven multi-agent project orchestration. + +Provides persistent project planning with coordinated agent execution. +""" diff --git a/scenarios/project_planner/__main__.py b/scenarios/project_planner/__main__.py new file mode 100644 index 00000000..9244b8f6 --- /dev/null +++ b/scenarios/project_planner/__main__.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +Project Planner CLI - Main entry point. + +Usage: + python -m scenarios.project_planner init --name "Project Name" + python -m scenarios.project_planner plan --goals "Build authentication system" + python -m scenarios.project_planner status + python -m scenarios.project_planner execute +""" + +import sys +from pathlib import Path + +# Add project root to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from scenarios.project_planner.cli import main + +if __name__ == "__main__": + main() diff --git a/scenarios/project_planner/cli.py b/scenarios/project_planner/cli.py new file mode 100644 index 00000000..57c74f19 --- /dev/null +++ b/scenarios/project_planner/cli.py @@ -0,0 +1,221 @@ +""" +Project Planner CLI implementation. + +Provides commands for initializing, planning, and executing projects with AI coordination. +""" + +import argparse +import sys +from pathlib import Path + +from amplifier.core.context import create_project_context +from amplifier.core.context import detect_project_context +from amplifier.planner import Project +from amplifier.planner import Task +from amplifier.planner import save_project + + +def cmd_init(args) -> None: + """Initialize a new project with planning.""" + project_root = Path(args.path) if args.path else Path.cwd() + + # Check if project already exists + existing = detect_project_context(project_root) + if existing: + print(f"❌ Project already exists: {existing.project_name}") + print(f" Located at: {existing.config_file}") + return + + # Create new project context + context = create_project_context(project_name=args.name, project_root=project_root, enable_planning=True) + + print(f"βœ… Created project: {context.project_name}") + print(f" Project ID: {context.project_id}") + print(f" Config: {context.config_file}") + print(f" Planning: {'Enabled' if context.has_planning else 'Disabled'}") + + +def cmd_plan(args) -> None: + """Plan project tasks with AI decomposition.""" + context = detect_project_context() + if not context: + print("❌ No project found. Run 'init' first.") + return + + if not context.has_planning: + print("❌ Planning not enabled for this project.") + print(" Enable with: --enable-planning") + return + + # Create or load planner project + if not context.planner_project: + planner_project = Project(id=context.project_id, name=context.project_name) + else: + planner_project = context.planner_project + + if args.goals: + print(f"🧠 Planning tasks for: {args.goals}") + + # Use smart decomposer for AI-driven task breakdown + import asyncio + + from amplifier.planner.decomposer import ProjectContext + from amplifier.planner.decomposer import decompose_goal + + # Create context for decomposition + decomposer_context = ProjectContext(project=planner_project, max_depth=3, min_tasks=2) + + # Run async decomposition + try: + tasks = asyncio.run(decompose_goal(args.goals, decomposer_context)) + + # Add tasks to project + for task in tasks: + planner_project.add_task(task) + + print(f"βœ… Generated {len(tasks)} tasks using AI decomposition") + + except Exception as e: + print(f"⚠️ AI decomposition failed, using simple task: {e}") + # Fallback to simple task + main_task = Task(id="main-goal", title=args.goals, description=f"Main project goal: {args.goals}") + planner_project.add_task(main_task) + + # Save the project + save_project(planner_project) + context.planner_project = planner_project + + print(f"βœ… Created planning structure with {len(planner_project.tasks)} tasks") + else: + print(f"πŸ“Š Current project status: {context.project_name}") + if context.planner_project: + print(f" Tasks: {len(context.planner_project.tasks)}") + else: + print(" No planning data yet") + + +def cmd_status(args) -> None: + """Show project status and progress.""" + context = detect_project_context() + if not context: + print("❌ No project found. Run 'init' first.") + return + + print(f"πŸ“Š Project Status: {context.project_name}") + print(f" Project ID: {context.project_id}") + print(f" Root: {context.project_root}") + print(f" Planning: {'Enabled' if context.has_planning else 'Disabled'}") + + if context.has_planning and context.planner_project: + project = context.planner_project + print(f" Tasks: {len(project.tasks)}") + + # Show task breakdown by state + from collections import Counter + + states = Counter(task.state.value for task in project.tasks.values()) + for state, count in states.items(): + print(f" {state}: {count}") + + # Show root tasks + roots = project.get_roots() + if roots: + print(" Root tasks:") + for root in roots[:5]: # Show first 5 + print(f" β€’ {root.title}") + else: + print(" No planning data") + + +def cmd_execute(args) -> None: + """Execute project tasks with agent coordination.""" + context = detect_project_context() + if not context: + print("❌ No project found. Run 'init' first.") + return + + if not context.has_planning or not context.planner_project: + print("❌ No planning data. Run 'plan' first.") + return + + print(f"πŸš€ Executing project: {context.project_name}") + + # Use orchestrator for intelligent task execution + import asyncio + + from amplifier.planner.orchestrator import orchestrate_execution + + project = context.planner_project + + # Run orchestrated execution + try: + results = asyncio.run(orchestrate_execution(project, max_parallel=args.max_parallel or 3)) + + print("βœ… Execution complete:") + print(f" β€’ Total tasks: {results.total_tasks}") + print(f" β€’ Completed: {results.completed_tasks}") + print(f" β€’ Failed: {results.failed_tasks}") + + # Update project state + save_project(project) + + return + + except Exception as e: + print(f"⚠️ Orchestrated execution failed: {e}") + + # Fallback to simple execution + completed = set() + + # Find tasks that can start + startable = [t for t in project.tasks.values() if t.can_start(completed)] + + if startable: + print(f" Ready to start: {len(startable)} tasks") + for task in startable[:3]: # Show first 3 + print(f" β€’ {task.title}") + else: + print(" No tasks ready to start") + + +def main() -> None: + """Main CLI entry point.""" + parser = argparse.ArgumentParser(prog="project-planner", description="AI-driven multi-agent project orchestration") + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Init command + init_parser = subparsers.add_parser("init", help="Initialize new project") + init_parser.add_argument("--name", required=True, help="Project name") + init_parser.add_argument("--path", help="Project root path (default: current directory)") + init_parser.set_defaults(func=cmd_init) + + # Plan command + plan_parser = subparsers.add_parser("plan", help="Plan project tasks") + plan_parser.add_argument("--goals", help="Project goals to plan for") + plan_parser.set_defaults(func=cmd_plan) + + # Status command + status_parser = subparsers.add_parser("status", help="Show project status") + status_parser.set_defaults(func=cmd_status) + + # Execute command + execute_parser = subparsers.add_parser("execute", help="Execute project tasks") + execute_parser.set_defaults(func=cmd_execute) + + # Parse and execute + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + try: + args.func(args) + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scenarios/smart_decomposer/README.md b/scenarios/smart_decomposer/README.md new file mode 100644 index 00000000..f32b99db --- /dev/null +++ b/scenarios/smart_decomposer/README.md @@ -0,0 +1,158 @@ +# Smart Decomposer CLI Tool + +An intelligent task decomposition and orchestration tool that breaks down high-level goals into actionable tasks and coordinates their execution using specialized AI agents. + +## Overview + +The Smart Decomposer provides a command-line interface for: +- **Decomposing** complex goals into hierarchical task structures +- **Assigning** specialized agents to tasks based on their capabilities +- **Executing** tasks with intelligent orchestration and dependency management +- **Tracking** project status and progress + +## Installation + +This tool is part of the amplifier project and uses the planner modules. + +```bash +# Run from the amplifier root directory +make smart-decomposer ARGS="--help" +``` + +## Usage + +### 1. Decompose a Goal + +Break down a high-level goal into specific tasks: + +```bash +# Basic usage +python -m scenarios.smart_decomposer decompose --goal "Build a REST API with authentication" + +# With custom project ID and name +python -m scenarios.smart_decomposer decompose \ + --goal "Build a REST API with authentication" \ + --project-id "api-project" \ + --project-name "API Development" \ + --max-depth 3 +``` + +### 2. Assign Agents + +Assign specialized agents to tasks based on their requirements: + +```bash +# Use default agent pool +python -m scenarios.smart_decomposer assign --project-id "api-project" + +# Specify custom agents +python -m scenarios.smart_decomposer assign \ + --project-id "api-project" \ + --agents "zen-code-architect,modular-builder,test-coverage" +``` + +### 3. Execute Tasks + +Execute the project with intelligent orchestration: + +```bash +# Normal execution +python -m scenarios.smart_decomposer execute --project-id "api-project" + +# With custom parallelism +python -m scenarios.smart_decomposer execute \ + --project-id "api-project" \ + --max-parallel 10 + +# Dry run to see what would be executed +python -m scenarios.smart_decomposer execute \ + --project-id "api-project" \ + --dry-run +``` + +### 4. Check Status + +View the current status of a project: + +```bash +# Basic status +python -m scenarios.smart_decomposer status --project-id "api-project" + +# Detailed status with task samples +python -m scenarios.smart_decomposer status --project-id "api-project" --verbose +``` + +## Available Agents + +The tool can assign the following specialized agents: + +- **zen-code-architect**: Architecture and design tasks +- **modular-builder**: Implementation and construction tasks +- **bug-hunter**: Debugging and issue resolution +- **test-coverage**: Testing and validation tasks +- **refactor-architect**: Code refactoring and optimization +- **integration-specialist**: System integration tasks + +## Project Storage + +Projects are stored locally in: +``` +~/.amplifier/smart_decomposer/projects/{project-id}.json +``` + +## Command Reference + +### Global Options + +- `-v, --verbose`: Show detailed output + +### Commands + +#### decompose +- `--goal GOAL`: The goal to decompose (required) +- `--project-id PROJECT_ID`: Custom project ID (auto-generated if not provided) +- `--project-name PROJECT_NAME`: Human-readable project name +- `--max-depth MAX_DEPTH`: Maximum decomposition depth (default: 3) + +#### assign +- `--project-id PROJECT_ID`: Project to assign agents to (required) +- `--agents AGENTS`: Comma-separated list of available agents + +#### execute +- `--project-id PROJECT_ID`: Project to execute (required) +- `--max-parallel N`: Maximum parallel task execution (default: 5) +- `--dry-run`: Simulate execution without running +- `--force`: Execute even with unassigned tasks + +#### status +- `--project-id PROJECT_ID`: Project to check status for (required) + +## Workflow Example + +Complete workflow for a project: + +```bash +# Step 1: Decompose the goal +python -m scenarios.smart_decomposer decompose \ + --goal "Build a user management system with authentication" \ + --project-id "user-system" + +# Step 2: Assign agents to tasks +python -m scenarios.smart_decomposer assign --project-id "user-system" + +# Step 3: Check the plan +python -m scenarios.smart_decomposer status --project-id "user-system" --verbose + +# Step 4: Execute the project +python -m scenarios.smart_decomposer execute --project-id "user-system" + +# Step 5: Check final status +python -m scenarios.smart_decomposer status --project-id "user-system" +``` + +## Integration + +This tool integrates with the amplifier planner modules: +- `amplifier.planner.decomposer`: Task decomposition logic +- `amplifier.planner.agent_mapper`: Agent assignment logic +- `amplifier.planner.orchestrator`: Execution orchestration \ No newline at end of file diff --git a/scenarios/smart_decomposer/__init__.py b/scenarios/smart_decomposer/__init__.py new file mode 100644 index 00000000..882abe18 --- /dev/null +++ b/scenarios/smart_decomposer/__init__.py @@ -0,0 +1,7 @@ +"""Smart Decomposer CLI Tool. + +A command-line interface for intelligent task decomposition and orchestration +using the amplifier planner modules. +""" + +__version__ = "1.0.0" diff --git a/scenarios/smart_decomposer/__main__.py b/scenarios/smart_decomposer/__main__.py new file mode 100644 index 00000000..3e9b6e28 --- /dev/null +++ b/scenarios/smart_decomposer/__main__.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +Smart Decomposer CLI - Main entry point. + +Usage: + python -m scenarios.smart_decomposer decompose --goal "Build feature X" + python -m scenarios.smart_decomposer assign --project-id "proj123" + python -m scenarios.smart_decomposer execute --project-id "proj123" + python -m scenarios.smart_decomposer status --project-id "proj123" +""" + +import sys +from pathlib import Path + +# Add project root to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from scenarios.smart_decomposer.cli import main + +if __name__ == "__main__": + main() diff --git a/scenarios/smart_decomposer/cli.py b/scenarios/smart_decomposer/cli.py new file mode 100644 index 00000000..3415ac97 --- /dev/null +++ b/scenarios/smart_decomposer/cli.py @@ -0,0 +1,293 @@ +""" +Smart Decomposer CLI implementation. + +Provides commands for decomposing goals, assigning agents, and orchestrating execution. +""" + +import argparse +import asyncio +import logging +import sys + +from amplifier.planner import Project +from amplifier.planner import TaskState +from amplifier.planner import load_project +from amplifier.planner import save_project +from amplifier.planner.agent_mapper import assign_agent +from amplifier.planner.decomposer import ProjectContext +from amplifier.planner.decomposer import decompose_goal +from amplifier.planner.orchestrator import orchestrate_execution + +# Set up logging +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# Projects are stored by the planner module in data/planner/projects/ + + +def cmd_decompose(args) -> None: + """Decompose a goal into tasks.""" + # Create or load project + if args.project_id: + try: + project = load_project(args.project_id) + logger.info(f"πŸ“‚ Loaded existing project: {project.name}") + except (FileNotFoundError, KeyError): + project = Project(id=args.project_id, name=args.project_name or args.project_id) + logger.info(f"✨ Created new project: {project.name}") + else: + import uuid + + project_id = str(uuid.uuid4())[:8] + project = Project(id=project_id, name=args.project_name or f"Project-{project_id}") + logger.info(f"✨ Created new project: {project.name} (ID: {project_id})") + + # Create context for decomposition + context = ProjectContext(project=project, max_depth=args.max_depth, min_tasks=2) + + logger.info(f"🧠 Decomposing goal: {args.goal}") + + # Run async decomposition + try: + tasks = asyncio.run(decompose_goal(args.goal, context)) + + # Add tasks to project + for task in tasks: + project.add_task(task) + + # Save project + save_project(project) + + logger.info(f"βœ… Generated {len(tasks)} tasks") + logger.info(f"πŸ“ Project saved with ID: {project.id}") + + # Display task hierarchy + if args.verbose: + logger.info("\nπŸ“‹ Task Hierarchy:") + for task in tasks[:5]: # Show first 5 + logger.info(f" β€’ {task.title}") + if task.description: + logger.info(f" {task.description[:100]}...") + + logger.info(f"\nπŸ’‘ Next step: Assign agents with 'assign --project-id {project.id}'") + + except Exception as e: + logger.error(f"❌ Decomposition failed: {e}") + sys.exit(1) + + +def cmd_assign(args) -> None: + """Assign agents to tasks.""" + try: + # Load project + project = load_project(args.project_id) + except (FileNotFoundError, KeyError): + logger.error(f"❌ Project not found: {args.project_id}") + logger.error(" Run 'decompose' first to create a project") + sys.exit(1) + logger.info(f"πŸ“‚ Loaded project: {project.name}") + + # Get available agents (hardcoded for now, could be made configurable) + available_agents = [ + "zen-code-architect", + "modular-builder", + "bug-hunter", + "test-coverage", + "refactor-architect", + "integration-specialist", + ] + + if args.agents: + # Use custom agent list if provided + available_agents = args.agents.split(",") + + logger.info(f"πŸ€– Assigning agents to {len(project.tasks)} tasks") + logger.info(f" Available agents: {', '.join(available_agents)}") + + # Assign agents to each task + assignments = {} + for task in project.tasks.values(): + agent = assign_agent(task, available_agents) + task.assigned_to = agent + assignments[task.title] = agent + + # Save updated project + save_project(project) + + logger.info("βœ… Agent assignments complete") + + # Show assignments + if args.verbose or len(assignments) <= 10: + logger.info("\nπŸ“‹ Task Assignments:") + for title, agent in list(assignments.items())[:10]: + logger.info(f" β€’ {title[:50]}... β†’ {agent}") + + logger.info(f"\nπŸ’‘ Next step: Execute with 'execute --project-id {project.id}'") + + +def cmd_execute(args) -> None: + """Execute project tasks with orchestration.""" + try: + # Load project + project = load_project(args.project_id) + except (FileNotFoundError, KeyError): + logger.error(f"❌ Project not found: {args.project_id}") + sys.exit(1) + logger.info(f"πŸ“‚ Loaded project: {project.name}") + + # Check if agents are assigned + unassigned = [t for t in project.tasks.values() if not t.assigned_to] + + if unassigned: + logger.warning(f"⚠️ {len(unassigned)} tasks have no agent assigned") + logger.warning(" Run 'assign' first to assign agents") + if not args.force: + sys.exit(1) + + logger.info(f"πŸš€ Executing {len(project.tasks)} tasks") + + if args.dry_run: + logger.info(" πŸ” DRY RUN - No actual execution") + + # Run orchestration + try: + results = asyncio.run(orchestrate_execution(project, max_parallel=args.max_parallel)) + + # Save updated project + save_project(project) + + # Display results + logger.info("\nβœ… Execution complete") + logger.info(f" Total tasks: {results.total_tasks}") + logger.info(f" Completed: {results.completed_tasks}") + logger.info(f" Failed: {results.failed_tasks}") + logger.info(f" Skipped: {results.skipped_tasks}") + + if results.failed_tasks > 0 and args.verbose: + logger.info("\n❌ Failed Tasks:") + for task_id, result in results.task_results.items(): + if result.status == "failed": + task = project.tasks.get(task_id) + if task: + logger.info(f" β€’ {task.title}: {result.error}") + + except Exception as e: + logger.error(f"❌ Execution failed: {e}") + sys.exit(1) + + +def cmd_status(args) -> None: + """Show project status and progress.""" + try: + # Load project + project = load_project(args.project_id) + except (FileNotFoundError, KeyError): + logger.error(f"❌ Project not found: {args.project_id}") + logger.info("\nTip: Use 'decompose' to create a new project") + sys.exit(1) + + logger.info(f"πŸ“Š Project Status: {project.name}") + logger.info(f" ID: {project.id}") + logger.info(f" Tasks: {len(project.tasks)}") + + # Count by state + from collections import Counter + + states = Counter(task.state.value for task in project.tasks.values()) + + logger.info("\nπŸ“ˆ Task States:") + for state in TaskState: + count = states.get(state.value, 0) + if count > 0: + icon = { + "pending": "⏳", + "ready": "βœ…", + "in_progress": "πŸ”„", + "completed": "βœ”οΈ", + "failed": "❌", + "blocked": "🚫", + }.get(state.value, "β€’") + logger.info(f" {icon} {state.value}: {count}") + + # Show agent assignments + agents = Counter(task.assigned_to or "unassigned" for task in project.tasks.values()) + + if len(agents) > 1 or "unassigned" not in agents: + logger.info("\nπŸ€– Agent Assignments:") + for agent, count in agents.most_common(): + logger.info(f" β€’ {agent}: {count} tasks") + + # Show sample tasks + if args.verbose: + logger.info("\nπŸ“‹ Sample Tasks:") + for task in list(project.tasks.values())[:5]: + agent = task.assigned_to or "unassigned" + logger.info(f" β€’ [{task.state.value}] {task.title[:60]}... ({agent})") + + # Show next steps + if states.get("pending", 0) > 0 and "unassigned" in agents: + logger.info(f"\nπŸ’‘ Next step: Assign agents with 'assign --project-id {project.id}'") + elif states.get("ready", 0) > 0 or states.get("pending", 0) > 0: + logger.info(f"\nπŸ’‘ Next step: Execute with 'execute --project-id {project.id}'") + elif states.get("completed", 0) == len(project.tasks): + logger.info("\nβœ… All tasks completed!") + + +def main() -> None: + """Main CLI entry point.""" + parser = argparse.ArgumentParser( + prog="smart-decomposer", description="Intelligent task decomposition and orchestration" + ) + + # Add global options + parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed output") + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Decompose command + decompose_parser = subparsers.add_parser("decompose", help="Decompose a goal into tasks") + decompose_parser.add_argument("--goal", required=True, help="Goal to decompose") + decompose_parser.add_argument("--project-id", help="Project ID (auto-generated if not provided)") + decompose_parser.add_argument("--project-name", help="Project name") + decompose_parser.add_argument("--max-depth", type=int, default=3, help="Maximum decomposition depth") + decompose_parser.set_defaults(func=cmd_decompose) + + # Assign command + assign_parser = subparsers.add_parser("assign", help="Assign agents to tasks") + assign_parser.add_argument("--project-id", required=True, help="Project ID") + assign_parser.add_argument("--agents", help="Comma-separated list of available agents") + assign_parser.set_defaults(func=cmd_assign) + + # Execute command + execute_parser = subparsers.add_parser("execute", help="Execute tasks with orchestration") + execute_parser.add_argument("--project-id", required=True, help="Project ID") + execute_parser.add_argument("--max-parallel", type=int, default=5, help="Maximum parallel tasks") + execute_parser.add_argument("--dry-run", action="store_true", help="Simulate execution without running") + execute_parser.add_argument("--force", action="store_true", help="Execute even with unassigned tasks") + execute_parser.set_defaults(func=cmd_execute) + + # Status command + status_parser = subparsers.add_parser("status", help="Show project status") + status_parser.add_argument("--project-id", required=True, help="Project ID") + status_parser.set_defaults(func=cmd_status) + + # Parse and execute + args = parser.parse_args() + + if not args.command: + parser.print_help() + return + + try: + args.func(args) + except Exception as e: + logger.error(f"❌ Error: {e}") + if args.verbose: + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scenarios/smart_decomposer/test_basic.py b/scenarios/smart_decomposer/test_basic.py new file mode 100644 index 00000000..2e090230 --- /dev/null +++ b/scenarios/smart_decomposer/test_basic.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Basic test to verify the smart_decomposer CLI works.""" + +import subprocess +import sys + + +def run_command(args): + """Run a command and return the result.""" + cmd = [sys.executable, "-m", "scenarios.smart_decomposer"] + args + result = subprocess.run(cmd, capture_output=True, text=True) + return result.returncode, result.stdout, result.stderr + + +def test_help(): + """Test help command.""" + code, stdout, stderr = run_command(["--help"]) + assert code == 0, f"Help failed: {stderr}" + assert "decompose" in stdout + assert "assign" in stdout + assert "execute" in stdout + assert "status" in stdout + print("βœ“ Help command works") + + +def test_status_non_existent(): + """Test status with non-existent project.""" + code, stdout, stderr = run_command(["status", "--project-id", "test-xyz-999"]) + assert code == 1, "Should fail for non-existent project" + assert "Project not found" in stderr + print("βœ“ Status correctly reports missing project") + + +def test_decompose_help(): + """Test decompose help.""" + code, stdout, stderr = run_command(["decompose", "--help"]) + assert code == 0, f"Decompose help failed: {stderr}" + assert "--goal" in stdout + assert "--project-id" in stdout + print("βœ“ Decompose help works") + + +def main(): + """Run all tests.""" + print("Testing smart_decomposer CLI...") + test_help() + test_status_non_existent() + test_decompose_help() + print("\nβœ… All basic tests passed!") + + +if __name__ == "__main__": + main()