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()
Comment thread
frankbria marked this conversation as resolved.

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)
Comment thread
frankbria marked this conversation as resolved.


# 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)

Comment thread
frankbria marked this conversation as resolved.
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)
Comment on lines +658 to +716

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the resolved template ID and clarify the section heading.

  • Return the actual template.id to avoid storing/logging a caller-supplied alias.
  • The heading says “Required Sections” but includes optional sections; consider renaming to “Sections” (or split required vs optional).
🔧 Suggested adjustment
-## Required Sections
+## Sections
@@
-        return (prompt, template_id)
+        return (prompt, template.id)
🤖 Prompt for AI Agents
In `@codeframe/core/prd_discovery.py` around lines 657 - 715, The prompt builder
_build_prd_prompt currently returns the caller-supplied template_id and labels
the section block "Required Sections" even when optional sections are included;
update it to return the resolved template.id (use template.id when returning the
tuple) and change the heading logic for template.sections so the prompt either
uses a neutral "Sections" heading or splits into "Required Sections" and
"Optional Sections" by iterating template.sections and grouping by
section.required; keep the rest of prompt construction and 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