fix: handle empty/greenfield projects in spec creation (#1426) (#1841)

* fix: handle empty/greenfield projects in spec creation and prevent stuck planning state (#1426)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review findings - planning_phase_ended bug, type hints, dedup (#1426)

- Convert planning_phase_ended to instance attribute self._planning_phase_ended
  so _run_phases() can mark it True after each end_phase() call, preventing
  double-end on exception propagation
- Add Path type annotation to _is_greenfield_project(spec_dir)
- Extract duplicated greenfield detection into _check_and_log_greenfield() helper
- Add TaskLogger and types.ModuleType type hints to _run_phases() signature
- Simplify redundant SystemExit handler with explanatory comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent greenfield false positive on missing/corrupt project index

When get_project_index_stats() returns {} (file missing, JSON parse
error, or unrecognized format), _is_greenfield_project() now returns
False instead of incorrectly classifying the project as greenfield.
Also removes unused TYPE_CHECKING import and empty conditional block.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-02-18 14:49:05 +01:00
committed by GitHub
parent 28a620079f
commit 819f98d9fa
3 changed files with 77 additions and 18 deletions
+7 -1
View File
@@ -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
+46 -4
View File
@@ -6,18 +6,54 @@ Phases for spec document creation and quality assurance.
"""
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 @@ class SpecPhaseMixin:
"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 @@ class SpecPhaseMixin:
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 @@ Create:
"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 @@ Create:
success, output = await self.run_agent_fn(
"spec_writer.md",
additional_context=greenfield_ctx,
phase_name="spec_writing",
)
+24 -13
View File
@@ -6,6 +6,7 @@ Main orchestration logic for spec creation with dynamic complexity adaptation.
"""
import json
import types
from collections.abc import Callable
from pathlib import Path
@@ -18,6 +19,7 @@ from review import run_review_checkpoint
from task_logger import (
LogEntryType,
LogPhase,
TaskLogger,
get_task_logger,
)
from ui import (
@@ -238,6 +240,9 @@ class SpecOrchestrator:
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:
@@ -251,22 +256,24 @@ class SpecOrchestrator:
)
except Exception:
pass # Don't mask the original error
try:
task_logger.end_phase(
LogPhase.PLANNING,
success=False,
message=f"Spec creation crashed: {e}",
)
except Exception:
pass # Best effort - don't mask the original error when logging fails
if not self._planning_phase_ended:
self._planning_phase_ended = True
try:
task_logger.end_phase(
LogPhase.PLANNING,
success=False,
message=f"Spec creation crashed: {e}",
)
except Exception:
pass # Best effort - don't mask the original error when logging fails
raise
async def _run_phases(
self,
interactive: bool,
auto_approve: bool,
task_logger,
ui,
task_logger: TaskLogger,
ui: types.ModuleType,
) -> bool:
"""Internal method that runs all spec creation phases.
@@ -327,6 +334,7 @@ class SpecOrchestrator:
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"
)
@@ -342,6 +350,7 @@ class SpecOrchestrator:
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,
@@ -380,6 +389,7 @@ class SpecOrchestrator:
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"
)
@@ -442,6 +452,7 @@ class SpecOrchestrator:
f"Phase '{phase_name}' failed: {'; '.join(result.errors)}",
LogEntryType.ERROR,
)
self._planning_phase_ended = True
task_logger.end_phase(
LogPhase.PLANNING,
success=False,
@@ -456,6 +467,7 @@ class SpecOrchestrator:
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"
)
@@ -729,9 +741,8 @@ class SpecOrchestrator:
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()