Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
166 changes: 164 additions & 2 deletions codeframe/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,142 @@ def serve(
no_args_is_help=True,
)

# PRD templates subcommand group
prd_templates_app = typer.Typer(
name="templates",
help="PRD template management for customizable output formats",
no_args_is_help=True,
)


@prd_templates_app.command("list")
def prd_templates_list() -> None:
"""List available PRD templates.

Shows all built-in and custom PRD templates that can be used
with 'codeframe prd generate --template'.

Example:
codeframe prd templates list
"""
from codeframe.planning.prd_templates import PrdTemplateManager

# Pass workspace path to include project templates
manager = PrdTemplateManager(workspace_path=Path.cwd())
templates = manager.list_templates()

console.print("\n[bold]Available PRD Templates:[/bold]\n")
for template in templates:
section_count = len(template.sections)
console.print(f" [green]{template.id}[/green] - {template.name}")
console.print(f" {template.description}")
console.print(f" Sections: {section_count} | Version: {template.version}")
console.print()


@prd_templates_app.command("show")
def prd_templates_show(
template_id: str = typer.Argument(..., help="Template ID to show"),
) -> None:
"""Show details of a specific PRD template.

Displays the template's sections and their configuration.

Example:
codeframe prd templates show standard
codeframe prd templates show lean
"""
from codeframe.planning.prd_templates import PrdTemplateManager

# Pass workspace path to include project templates
manager = PrdTemplateManager(workspace_path=Path.cwd())
template = manager.get_template(template_id)

if not template:
console.print(f"[red]Error:[/red] Template '{template_id}' not found.")
console.print("\nAvailable templates:")
for t in manager.list_templates():
console.print(f" {t.id}")
raise typer.Exit(1)

console.print(f"\n[bold]{template.name}[/bold] ({template.id})\n")
console.print(f"{template.description}\n")
console.print(f"Version: {template.version}")

console.print("\n[bold]Sections:[/bold]\n")
for i, section in enumerate(template.sections, 1):
required = "[green]required[/green]" if section.required else "[dim]optional[/dim]"
console.print(f" {i}. {section.title} ({section.id})")
console.print(f" Source: {section.source} | {required}")
console.print()


@prd_templates_app.command("export")
def prd_templates_export(
template_id: str = typer.Argument(..., help="Template ID to export"),
output_path: Path = typer.Argument(..., help="Output file path (.yaml)"),
) -> None:
"""Export a PRD template to a YAML file.

Exports the template configuration for backup or customization.

Example:
codeframe prd templates export standard ./my-template.yaml
codeframe prd templates export enterprise ./enterprise-prd.yaml
"""
from codeframe.planning.prd_templates import PrdTemplateManager

# Pass workspace path to include project templates
manager = PrdTemplateManager(workspace_path=Path.cwd())

try:
manager.export_template(template_id, output_path)
console.print(f"[green]✓[/green] Exported template '{template_id}' to {output_path}")
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
console.print("\nAvailable templates:")
for t in manager.list_templates():
console.print(f" {t.id}")
raise typer.Exit(1)


@prd_templates_app.command("import")
def prd_templates_import(
source_path: Path = typer.Argument(
...,
help="Path to template YAML file",
exists=True,
file_okay=True,
dir_okay=False,
),
) -> None:
"""Import a PRD template from a YAML file.

Imports a custom template that can be used with 'codeframe prd generate --template'.
The template is saved to the project's .codeframe/templates/prd/ directory.

Example:
codeframe prd templates import ./custom-template.yaml
"""
from codeframe.planning.prd_templates import PrdTemplateManager

# Pass workspace path for project template storage
manager = PrdTemplateManager(workspace_path=Path.cwd())

try:
# Import and persist to project directory
template = manager.import_template(source_path, persist=True)
console.print(f"[green]✓[/green] Imported template '{template.id}' ({template.name})")
console.print(f"[dim]Sections: {len(template.sections)}[/dim]")
console.print(f"[dim]Saved to: .codeframe/templates/prd/{template.id}.yaml[/dim]")
except Exception as e:
console.print(f"[red]Error:[/red] Failed to import template: {e}")
raise typer.Exit(1)


# Register prd_templates_app under prd_app
prd_app.add_typer(prd_templates_app, name="templates")


@prd_app.command("add")
def prd_add(
Expand Down Expand Up @@ -1104,6 +1240,11 @@ def prd_generate(
"--resume", "-r",
help="Resume from a paused session (blocker ID)",
),
template: str = typer.Option(
"standard",
"--template", "-t",
help="PRD template to use (standard, lean, enterprise, user-story-map, technical-spec)",
),
) -> None:
"""Generate a PRD through AI-driven Socratic discovery.

Expand All @@ -1121,10 +1262,18 @@ def prd_generate(
/quit - Exit without saving
/help - Show available commands

Use --template to select the output format:
- standard: Full PRD with all sections (default)
- lean: Minimal PRD with problem, users, MVP features
- enterprise: Formal PRD with compliance and traceability
- user-story-map: Organized around user journeys
- technical-spec: Focused on technical specifications

Requires ANTHROPIC_API_KEY environment variable.

Example:
codeframe prd generate
codeframe prd generate --template lean
codeframe prd generate --resume abc123
"""
from codeframe.core.workspace import get_workspace
Expand All @@ -1137,11 +1286,24 @@ def prd_generate(
get_active_session,
)
from codeframe.core.events import emit_for_workspace, EventType
from codeframe.planning.prd_templates import PrdTemplateManager
from rich.panel import Panel
from rich.prompt import Prompt

workspace_path = repo_path or Path.cwd()

# Validate template exists (include project templates in search)
template_manager = PrdTemplateManager(workspace_path=workspace_path)
template_obj = template_manager.get_template(template)
if template_obj is None:
console.print(f"[red]Error:[/red] Template '{template}' not found.")
console.print("\nAvailable templates:")
for t in template_manager.list_templates():
console.print(f" {t.id} - {t.name}")
raise typer.Exit(1)

console.print(f"[dim]Using template: {template_obj.name}[/dim]")

try:
workspace = get_workspace(workspace_path)

Expand Down Expand Up @@ -1272,10 +1434,10 @@ def prd_generate(

# Generate PRD
console.print("\n[bold green]Discovery complete![/bold green]")
console.print("\nGenerating PRD from our conversation...")
console.print(f"\nGenerating PRD using '{template}' template...")

try:
prd_record = session.generate_prd()
prd_record = session.generate_prd(template_id=template)
except IncompleteSessionError as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(1)
Expand Down
95 changes: 83 additions & 12 deletions codeframe/core/prd_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,11 +589,16 @@ def resume_discovery(self, blocker_id: str) -> None:

logger.info(f"Resumed session {session_id} from blocker {blocker_id}")

def generate_prd(self) -> prd.PrdRecord:
def generate_prd(self, template_id: Optional[str] = None) -> prd.PrdRecord:
"""Generate PRD from discovery conversation.

Uses AI to synthesize the conversation into a structured PRD.

Args:
template_id: Optional PRD template ID to use for formatting.
If not provided or not found, uses the default built-in
prompt format (recorded as "default" in metadata).

Returns:
Created PrdRecord

Expand All @@ -606,10 +611,10 @@ def generate_prd(self) -> prd.PrdRecord:
f"Current coverage: {self._coverage}"
)


qa_history = self._format_qa_history()

prompt = PRD_GENERATION_PROMPT.format(qa_history=qa_history)
# Build prompt based on template and get the resolved template ID
prompt, resolved_template_id = self._build_prd_prompt(qa_history, template_id)

response = self._llm_provider.complete(
messages=[{"role": "user", "content": prompt}],
Expand All @@ -623,27 +628,93 @@ def generate_prd(self) -> prd.PrdRecord:
# Extract title from PRD content
title = self._extract_title_from_prd(content)

# Store PRD
# Store PRD with both requested and resolved template IDs in metadata
metadata: dict[str, Any] = {
"source": "ai_discovery",
"session_id": self.session_id,
"questions_asked": len(self._qa_history),
"coverage": self._coverage,
"generated_at": _utc_now().isoformat(),
"template_id": resolved_template_id,
}
# Track if a different template was requested but not found
if template_id and template_id != resolved_template_id:
metadata["requested_template_id"] = template_id

record = prd.store(
self.workspace,
content=content,
title=title,
metadata={
"source": "ai_discovery",
"session_id": self.session_id,
"questions_asked": len(self._qa_history),
"coverage": self._coverage,
"generated_at": _utc_now().isoformat(),
},
metadata=metadata,
)

# Update session state
self.state = SessionState.COMPLETED
self._save_session()

logger.info(f"Generated PRD {record.id} from session {self.session_id}")
logger.info(f"Generated PRD {record.id} from session {self.session_id} using template '{resolved_template_id}'")
return record

def _build_prd_prompt(
self, qa_history: str, template_id: Optional[str] = None
) -> tuple[str, str]:
"""Build PRD generation prompt based on template.

Args:
qa_history: Formatted Q&A history string
template_id: Template ID to use (defaults to None, which uses default prompt)

Returns:
Tuple of (prompt string, resolved template ID)
The resolved template ID is "default" if no template was used,
or the actual template ID that was successfully loaded.
"""
from codeframe.planning.prd_templates import PrdTemplateManager
from pathlib import Path

# Use default prompt if no template specified
if not template_id:
return (PRD_GENERATION_PROMPT.format(qa_history=qa_history), "default")

# Pass workspace path to include project templates
workspace_path = Path(self.workspace.repo_path) if self.workspace.repo_path else None
manager = PrdTemplateManager(workspace_path=workspace_path)
template = manager.get_template(template_id)

if template is None:
logger.warning(f"Template '{template_id}' not found, falling back to default prompt")
return (PRD_GENERATION_PROMPT.format(qa_history=qa_history), "default")

# Build dynamic prompt from template sections
sections_spec = []
for section in template.sections:
required_note = " (required)" if section.required else " (optional)"
sections_spec.append(f"## {section.title}{required_note}\n{section.source} - related content")

sections_text = "\n\n".join(sections_spec)

prompt = f"""Generate a Product Requirements Document based on the discovery conversation.

## Discovery Conversation
{qa_history}

## Template: {template.name}
{template.description}

## Sections
Generate a markdown PRD with these sections in order:

# [Project Title - infer from conversation]

{sections_text}

---

Keep it concise but complete. Focus on actionable requirements.
Follow the template structure exactly. This PRD should be sufficient to generate development tasks."""

return (prompt, template_id)

def _extract_title_from_prd(self, content: str) -> str:
"""Extract project title from generated PRD content."""
import re
Expand Down
11 changes: 11 additions & 0 deletions codeframe/planning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@
- Issue generation from PRD features
- Task decomposition from issues
- Work breakdown planning
- PRD template system for customizable output formats
"""

from codeframe.planning.issue_generator import (
IssueGenerator,
parse_prd_features,
assign_priority,
)
from codeframe.planning.prd_templates import (
PrdTemplate,
PrdTemplateSection,
PrdTemplateManager,
BUILTIN_TEMPLATES,
)

__all__ = [
"IssueGenerator",
"parse_prd_features",
"assign_priority",
"PrdTemplate",
"PrdTemplateSection",
"PrdTemplateManager",
"BUILTIN_TEMPLATES",
]
Loading
Loading