diff --git a/apps/backend/prompts/spec_writer.md b/apps/backend/prompts/spec_writer.md index bca7cca1bd..49c009b301 100644 --- a/apps/backend/prompts/spec_writer.md +++ b/apps/backend/prompts/spec_writer.md @@ -24,7 +24,7 @@ You MUST create `spec.md` with ALL required sections (see template below). ## PHASE 0: LOAD ALL CONTEXT (MANDATORY) ```bash -# Read all input files +# Read all input files (some may not exist for greenfield/empty projects) cat project_index.json cat requirements.json cat context.json @@ -35,6 +35,12 @@ Extract from these files: - **From requirements.json**: Task description, workflow type, services, acceptance criteria - **From context.json**: Files to modify, files to reference, patterns +**IMPORTANT**: If any input file is missing, empty, or shows 0 files, this is likely a **greenfield/new project**. Adapt accordingly: +- Skip sections that reference existing code (e.g., "Files to Modify", "Patterns to Follow") +- Instead, focus on files to CREATE and the initial project structure +- Define the tech stack, dependencies, and setup instructions from scratch +- Use industry best practices as patterns rather than referencing existing code + --- ## PHASE 1: ANALYZE CONTEXT diff --git a/apps/backend/spec/phases/spec_phases.py b/apps/backend/spec/phases/spec_phases.py index 1c39daf279..afb5e1a29e 100644 --- a/apps/backend/spec/phases/spec_phases.py +++ b/apps/backend/spec/phases/spec_phases.py @@ -6,18 +6,54 @@ """ import json -from typing import TYPE_CHECKING +from pathlib import Path from .. import validator, writer +from ..discovery import get_project_index_stats from .models import MAX_RETRIES, PhaseResult -if TYPE_CHECKING: - pass + +def _is_greenfield_project(spec_dir: Path) -> bool: + """Check if the project is empty/greenfield (0 discovered files).""" + stats = get_project_index_stats(spec_dir) + if not stats: + return False # Can't determine - don't assume greenfield + return stats.get("file_count", 0) == 0 + + +def _greenfield_context() -> str: + """Return additional context for greenfield/empty projects.""" + return """ +**GREENFIELD PROJECT**: This is an empty or new project with no existing code. +There are no existing files to reference or modify. You are creating everything from scratch. + +Adapt your approach: +- Do NOT reference existing files, patterns, or code structures +- Focus on what needs to be CREATED, not modified +- Define the initial project structure, files, and directories +- Specify the tech stack, frameworks, and dependencies to install +- Provide setup instructions for the new project +- For "Files to Modify" and "Files to Reference" sections, list files to CREATE instead +- For "Patterns to Follow", describe industry best practices rather than existing code +""" class SpecPhaseMixin: """Mixin for spec writing and critique phase methods.""" + def _check_and_log_greenfield(self) -> bool: + """Check if the project is greenfield and log if so. + + Returns: + True if the project is greenfield (no existing files). + """ + is_greenfield = _is_greenfield_project(self.spec_dir) + if is_greenfield: + self.ui.print_status( + "Greenfield project detected - adapting spec for new project", "info" + ) + return is_greenfield + async def phase_quick_spec(self) -> PhaseResult: """Quick spec for simple tasks - combines context and spec in one step.""" spec_file = self.spec_dir / "spec.md" @@ -29,6 +65,8 @@ async def phase_quick_spec(self) -> PhaseResult: "quick_spec", True, [str(spec_file), str(plan_file)], [], 0 ) + is_greenfield = self._check_and_log_greenfield() + errors = [] for attempt in range(MAX_RETRIES): self.ui.print_status( @@ -42,7 +80,7 @@ async def phase_quick_spec(self) -> PhaseResult: This is a SIMPLE task. Create a minimal spec and implementation plan directly. No research or extensive analysis needed. - +{_greenfield_context() if is_greenfield else ""} Create: 1. A concise spec.md with just the essential sections 2. A simple implementation_plan.json with 1-2 subtasks @@ -80,6 +118,9 @@ async def phase_spec_writing(self) -> PhaseResult: "spec.md exists but has issues, regenerating...", "warning" ) + is_greenfield = self._check_and_log_greenfield() + greenfield_ctx = _greenfield_context() if is_greenfield else "" + errors = [] for attempt in range(MAX_RETRIES): self.ui.print_status( @@ -88,6 +129,7 @@ async def phase_spec_writing(self) -> PhaseResult: success, output = await self.run_agent_fn( "spec_writer.md", + additional_context=greenfield_ctx, phase_name="spec_writing", ) diff --git a/apps/backend/spec/pipeline/orchestrator.py b/apps/backend/spec/pipeline/orchestrator.py index e8dc040ef5..44b5bb08fa 100644 --- a/apps/backend/spec/pipeline/orchestrator.py +++ b/apps/backend/spec/pipeline/orchestrator.py @@ -6,6 +6,7 @@ """ import json +import types from collections.abc import Callable from pathlib import Path @@ -18,6 +19,7 @@ from task_logger import ( LogEntryType, LogPhase, + TaskLogger, get_task_logger, ) from ui import ( @@ -238,6 +240,41 @@ async def run(self, interactive: bool = True, auto_approve: bool = False) -> boo task_logger.start_phase(LogPhase.PLANNING, "Starting spec creation process") TaskEventEmitter.from_spec_dir(self.spec_dir).emit("PLANNING_STARTED") + # Track whether we've already ended the planning phase (to avoid double-end) + self._planning_phase_ended = False + + try: + return await self._run_phases(interactive, auto_approve, task_logger, ui) + except Exception as e: + # Ensure planning phase is always properly ended on unexpected errors + # This prevents the task from being stuck in "active" planning state + if not self._planning_phase_ended: + self._planning_phase_ended = True + task_logger.end_phase( + LogPhase.PLANNING, + success=False, + message=f"Spec creation failed with unexpected error: {e}", + ) + raise + + async def _run_phases( + self, + interactive: bool, + auto_approve: bool, + task_logger: TaskLogger, + ui: types.ModuleType, + ) -> bool: + """Execute all spec creation phases. + + Args: + interactive: Whether to run in interactive mode + auto_approve: Whether to skip human review + task_logger: The task logger instance + ui: The UI module + + Returns: + True if spec creation and review completed successfully + """ print( box( f"Spec Directory: {self.spec_dir}\n" @@ -291,6 +328,7 @@ def run_phase(name: str, phase_fn: Callable) -> phases.PhaseResult: results.append(result) if not result.success: print_status("Discovery failed", "error") + self._planning_phase_ended = True task_logger.end_phase( LogPhase.PLANNING, success=False, message="Discovery failed" ) @@ -305,6 +343,7 @@ def run_phase(name: str, phase_fn: Callable) -> phases.PhaseResult: results.append(result) if not result.success: print_status("Requirements gathering failed", "error") + self._planning_phase_ended = True task_logger.end_phase( LogPhase.PLANNING, success=False, @@ -335,6 +374,7 @@ def run_phase(name: str, phase_fn: Callable) -> phases.PhaseResult: results.append(result) if not result.success: print_status("Complexity assessment failed", "error") + self._planning_phase_ended = True task_logger.end_phase( LogPhase.PLANNING, success=False, message="Complexity assessment failed" ) @@ -396,6 +436,7 @@ def run_phase(name: str, phase_fn: Callable) -> phases.PhaseResult: f"Phase '{phase_name}' failed: {'; '.join(result.errors)}", LogEntryType.ERROR, ) + self._planning_phase_ended = True task_logger.end_phase( LogPhase.PLANNING, success=False, @@ -407,6 +448,7 @@ def run_phase(name: str, phase_fn: Callable) -> phases.PhaseResult: self._print_completion_summary(results, phases_executed) # End planning phase successfully + self._planning_phase_ended = True task_logger.end_phase( LogPhase.PLANNING, success=True, message="Spec creation complete" ) @@ -661,9 +703,8 @@ def _run_review_checkpoint(self, auto_approve: bool) -> bool: print_status("Build will not proceed without approval.", "warning") return False - except SystemExit as e: - if e.code != 0: - return False + except SystemExit: + # Review checkpoint may call sys.exit(); treat any exit as unapproved return False except KeyboardInterrupt: print()