Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/backend/prompts/spec_writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
43 changes: 42 additions & 1 deletion apps/backend/spec/phases/spec_phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,36 @@
from typing import TYPE_CHECKING

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) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and code clarity, please add a type hint for the spec_dir parameter. Based on its usage, it should be Path. You can use a forward reference "Path" and add from pathlib import Path inside the if TYPE_CHECKING: block to avoid potential circular import issues while keeping the type checker happy.

Suggested change
def _is_greenfield_project(spec_dir) -> bool:
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)
return stats.get("file_count", 0) == 0
Comment on lines +16 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good fix for the false-positive greenfield detection on missing/corrupt index.

The if not stats: return False guard correctly prevents misclassifying projects when get_project_index_stats returns {} due to a missing or unparsable index file.

One remaining edge case: if project_index.json exists and parses as valid JSON but uses an unrecognized format (no "files" or "services" key), get_project_index_stats returns {"file_count": 0, ...} — a non-empty dict that passes the guard and yields file_count == 0 → True. This would be a false positive on a valid-but-unrecognized index format. Consider additionally checking that "file_count" is actually present in stats as a key (rather than relying on the default):

Proposed hardening
 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
+    if "file_count" not in stats:
+        return False  # Unrecognized format - don't assume greenfield
+    return stats["file_count"] == 0
🤖 Prompt for AI Agents
In `@apps/backend/spec/phases/spec_phases.py` around lines 16 - 21, The current
_is_greenfield_project uses get_project_index_stats and treats any non-empty
dict as valid; to avoid false positives when the index has an unrecognized
format, first verify that the "file_count" key exists in the returned stats
(e.g., check '"file_count" in stats') before comparing its value, and return
False if that key is missing so only explicit file_count values are treated as
greenfield indicators.



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."""

Expand All @@ -29,6 +53,13 @@ async def phase_quick_spec(self) -> PhaseResult:
"quick_spec", True, [str(spec_file), str(plan_file)], [], 0
)

is_greenfield = _is_greenfield_project(self.spec_dir)
if is_greenfield:
self.ui.print_status(
"Greenfield project detected - adapting spec for new project",
"info",
)

errors = []
for attempt in range(MAX_RETRIES):
self.ui.print_status(
Expand All @@ -42,7 +73,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
Expand Down Expand Up @@ -80,6 +111,15 @@ async def phase_spec_writing(self) -> PhaseResult:
"spec.md exists but has issues, regenerating...", "warning"
)

is_greenfield = _is_greenfield_project(self.spec_dir)
if is_greenfield:
self.ui.print_status(
"Greenfield project detected - adapting spec for new project",
"info",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for detecting a greenfield project and logging a status message is duplicated from phase_quick_spec (lines 56-61). To improve maintainability and reduce redundancy, consider extracting this logic into a private helper method within the SpecPhaseMixin class.

For example:

def _check_and_log_greenfield(self) -> bool:
    """Checks for a greenfield project and logs a status message if detected."""
    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

You could then replace the duplicated blocks in both phase_quick_spec and phase_spec_writing with a single call:
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(
Expand All @@ -88,6 +128,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",
)

Expand Down
35 changes: 35 additions & 0 deletions apps/backend/spec/pipeline/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,41 @@
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)
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 planning_phase_ended:
planning_phase_ended = True
task_logger.end_phase(
LogPhase.PLANNING,
success=False,
message=f"Spec creation failed with unexpected error: {e}",
)
raise
Comment on lines 243 to 258
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The try...except block is a great addition for ensuring the planning phase always terminates. However, the planning_phase_ended flag is a local variable in the run method and is not shared with _run_phases. This means if _run_phases calls task_logger.end_phase() and then an unexpected exception occurs before it returns, task_logger.end_phase() will be called a second time in this except block, which could lead to issues.

To fix this, planning_phase_ended should be an instance variable (e.g., self.planning_phase_ended):

  1. Initialize self.planning_phase_ended = False in __init__ or at the start of run.
  2. In _run_phases, set self.planning_phase_ended = True immediately after every call to task_logger.end_phase().
  3. In this except block, check if not self.planning_phase_ended.


async def _run_phases(
self,
interactive: bool,
auto_approve: bool,
task_logger,
ui,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve code clarity and maintainability, please add type hints for the task_logger and ui parameters.

You'll need to add the following imports at the top of the file:

from types import ModuleType
from task_logger import TaskLogger # Can be added to existing import from task_logger
Suggested change
task_logger,
ui,
task_logger: "TaskLogger",
ui: "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
Comment on lines +271 to +276

This comment was marked as outdated.

"""
print(
box(
f"Spec Directory: {self.spec_dir}\n"
Expand Down
Loading