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
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{"id":"codeframe-17","title":"codeframe-14.2: Frontend Chat Component","description":"Build React chat component with message input, display area, auto-scroll, typing indicators, and error handling. Include WebSocket connection for real-time updates. Style with Tailwind CSS matching dashboard design.","acceptance_criteria":"Component renders messages, input sends to API, WebSocket receives updates, UI matches design, 7 tests passing","notes":"codeframe-14.2 complete: Frontend ChatInterface.tsx component (227 lines) with message history, real-time WebSocket updates, loading states, optimistic UI. TypeScript 0 errors. 8 test specs documented.","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-10-16T16:19:17.061435037-07:00","updated_at":"2025-10-16T22:52:10.170827311-07:00","closed_at":"2025-10-16T22:52:10.170827311-07:00","labels":["frontend","p0","sprint-2"],"dependencies":[{"issue_id":"codeframe-17","depends_on_id":"codeframe-16","type":"blocks","created_at":"2025-10-16T16:19:17.062318143-07:00","created_by":"frankbria"}]}
{"id":"codeframe-18","title":"codeframe-14.3: Message Persistence","description":"Implement database schema and operations for chat message persistence. Create messages table with fields: id, project_id, role (user/assistant), content, timestamp. Add CRUD operations and database integration tests.","acceptance_criteria":"Messages table created, messages persist across sessions, queries work efficiently, 5 tests passing","notes":"codeframe-14.3 complete: Message persistence using memory table with role (user/assistant) and timestamps. Pagination support, chronological ordering (ORDER BY id). Covered in test_chat_api.py tests.","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-10-16T16:19:22.522108744-07:00","updated_at":"2025-10-16T22:52:10.248013626-07:00","closed_at":"2025-10-16T22:52:10.248013626-07:00","labels":["backend","database","p0","sprint-2"],"dependencies":[{"issue_id":"codeframe-18","depends_on_id":"codeframe-16","type":"blocks","created_at":"2025-10-16T16:19:22.52316183-07:00","created_by":"frankbria"}]}
{"id":"codeframe-19","title":"codeframe-15: Socratic Discovery Flow","description":"Implement Socratic discovery methodology: question framework generation, answer capture with structured metadata, Lead Agent integration for intelligent follow-ups. Enable conversational requirements gathering through progressive questioning.","acceptance_criteria":"Discovery questions generated, answers captured with structure, Lead Agent adapts questions, conversation flows naturally, 30 tests passing","notes":"codeframe-15 (Socratic Discovery Flow) complete. Discovery question framework (codeframe-15.1), answer capture \u0026 structuring (codeframe-15.2), and Lead Agent integration (codeframe-15.3) all implemented. 72 tests passing (100% pass rate), \u003e95% coverage. Multi-agent parallel execution with TDD.","status":"closed","priority":0,"issue_type":"feature","created_at":"2025-10-16T16:19:26.802269208-07:00","updated_at":"2025-10-16T22:51:50.590222809-07:00","closed_at":"2025-10-16T22:51:50.590222809-07:00","labels":["ai","full-stack","p0","sprint-2"]}
{"id":"codeframe-1j32","title":"Make sprint number configurable in planning automation","description":"Planning automation hardcodes sprint_number=1 in discovery.py:193. Should derive from project state or be configurable for multi-sprint support. See GitHub #210.","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T21:09:24.624768065-07:00","updated_at":"2026-01-06T21:09:24.624768065-07:00","labels":["enhancement","planning"]}
{"id":"codeframe-1vn","title":"T003: Add Blocker Pydantic models","description":"Add Blocker, BlockerCreate, BlockerResolve Pydantic models to codeframe/core/models.py","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-08T19:20:00.048395471-07:00","updated_at":"2025-11-14T10:48:48.464327998-07:00","closed_at":"2025-11-14T10:48:48.464327998-07:00","close_reason":"Sprint 6 (049-human-in-loop) complete - merged in PR #18"}
{"id":"codeframe-1ww","title":"Context Engineering Improvements","description":"Collection of context window optimization and management improvements. See GitHub issues #62-73.","status":"open","priority":2,"issue_type":"epic","created_at":"2026-01-06T17:26:16.851552468-07:00","updated_at":"2026-01-06T17:26:16.851552468-07:00","labels":["architecture","context-engineering"]}
{"id":"codeframe-1ww.1","title":"Evaluate Just-in-Time Context Loading Strategy","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-06T17:26:30.548499298-07:00","updated_at":"2026-01-06T17:26:30.548499298-07:00","external_ref":"gh-62","labels":["context-engineering"],"dependencies":[{"issue_id":"codeframe-1ww.1","depends_on_id":"codeframe-1ww","type":"parent-child","created_at":"2026-01-06T17:26:30.54903234-07:00","created_by":"daemon"}]}
Expand Down
98 changes: 95 additions & 3 deletions codeframe/ui/routers/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
allowing submission of discovery answers and retrieval of discovery progress.
"""

import asyncio
import os
import logging
from typing import Dict, Any
Expand All @@ -17,6 +18,13 @@
from codeframe.auth.dependencies import get_current_user
from codeframe.auth.models import User
from codeframe.ui.shared import manager
from codeframe.ui.websocket_broadcasts import (
broadcast_planning_started,
broadcast_issues_generated,
broadcast_tasks_decomposed,
broadcast_tasks_ready,
broadcast_planning_failed,
)

# Module logger
logger = logging.getLogger(__name__)
Expand All @@ -33,14 +41,13 @@ async def generate_prd_background(project_id: int, db: Database, api_key: str):
3. prd_generation_progress (calling_llm) - Sending to Claude API
4. prd_generation_progress (saving) - Saving PRD to database/file
5. prd_generation_completed - Final notification
6. Spawns planning automation as a separate async task

Args:
project_id: Project ID
db: Database instance
api_key: API key for Claude
"""
import asyncio

async def broadcast_progress(stage: str, message: str, progress_pct: int = 0):
"""Helper to broadcast progress updates."""
await manager.broadcast(
Expand Down Expand Up @@ -127,8 +134,21 @@ async def broadcast_progress(stage: str, message: str, progress_pct: int = 0):
project_id=project_id,
)

# Trigger planning automation as a non-blocking background task
# This allows PRD completion to be reported immediately while planning runs
def _handle_planning_exception(task: asyncio.Task) -> None:
"""Log any unhandled exceptions from background planning task."""
if not task.cancelled() and task.exception():
logger.error(
f"Unhandled exception in planning background task: {task.exception()}",
exc_info=task.exception()
)

planning_task = asyncio.create_task(generate_planning_background(project_id, db, api_key))
planning_task.add_done_callback(_handle_planning_exception)

except Exception as e:
logger.error(f"Failed to generate PRD for project {project_id}: {e}")
logger.error(f"Failed to generate PRD for project {project_id}: {e}", exc_info=True)
# Broadcast error
await manager.broadcast(
{
Expand All @@ -141,6 +161,78 @@ async def broadcast_progress(stage: str, message: str, progress_pct: int = 0):
)


async def generate_planning_background(project_id: int, db: Database, api_key: str):
"""Background task to generate issues and tasks after PRD completion.

This function implements planning automation:
1. planning_started - Notify automation begins
2. generate_issues - Create issues from PRD
3. issues_generated - Report issues created
4. decompose_prd - Decompose issues into tasks
5. tasks_decomposed - Report tasks created
6. tasks_ready - Signal ready for user review

Note: LeadAgent methods are synchronous and use the sync Anthropic client,
so asyncio.to_thread() is appropriate for running them without blocking.

Args:
project_id: Project ID
db: Database instance
api_key: API key for Claude
"""
# Configurable timeout for AI operations (default 2 minutes per operation)
planning_timeout = float(os.environ.get("PLANNING_OPERATION_TIMEOUT", "120"))

try:
logger.info(f"Starting planning automation for project {project_id}")

# Stage 1: Broadcast planning started
await broadcast_planning_started(manager, project_id)

# Initialize LeadAgent for issue/task generation
agent = LeadAgent(project_id=project_id, db=db, api_key=api_key)

# Stage 2: Generate issues from PRD (with timeout)
logger.info(f"Generating issues for project {project_id}")
issues = await asyncio.wait_for(
asyncio.to_thread(agent.generate_issues, sprint_number=1),
timeout=planning_timeout
)
issue_count = len(issues) if issues else 0

# Stage 3: Broadcast issues generated
await broadcast_issues_generated(manager, project_id, issue_count)
logger.info(f"Generated {issue_count} issues for project {project_id}")

# Stage 4: Decompose PRD into tasks (with timeout)
logger.info(f"Decomposing PRD into tasks for project {project_id}")
decomposition_result = await asyncio.wait_for(
asyncio.to_thread(agent.decompose_prd),
timeout=planning_timeout
)
task_count = decomposition_result.get("tasks", 0) if decomposition_result else 0

# Stage 5: Broadcast tasks decomposed
await broadcast_tasks_decomposed(manager, project_id, task_count)
logger.info(f"Decomposed into {task_count} tasks for project {project_id}")

# Stage 6: Broadcast tasks ready for review
await broadcast_tasks_ready(manager, project_id, task_count)
logger.info(f"Planning automation completed for project {project_id}")

except asyncio.TimeoutError:
logger.error(
f"Planning automation timed out for project {project_id} "
f"(timeout={planning_timeout}s)",
exc_info=True
)
await broadcast_planning_failed(manager, project_id, "Planning operation timed out")

except Exception as e:
logger.error(f"Planning automation failed for project {project_id}: {e}", exc_info=True)
await broadcast_planning_failed(manager, project_id, str(e))

Comment thread
frankbria marked this conversation as resolved.

@router.post("/answer")
async def submit_discovery_answer(
project_id: int,
Expand Down
141 changes: 140 additions & 1 deletion codeframe/ui/routers/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@
- Task creation
- Task updates
- Task status management
- Task approval (for planning phase automation)
"""

import logging
from typing import Optional
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from codeframe.core.models import Task, TaskStatus
from codeframe.core.phase_manager import PhaseManager
from codeframe.persistence.database import Database
from codeframe.ui.dependencies import get_db
from codeframe.ui.shared import manager
from codeframe.ui.websocket_broadcasts import broadcast_development_started
from codeframe.auth.dependencies import get_current_user
from codeframe.auth.models import User

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/tasks", tags=["tasks"])

# Also register under project-scoped prefix for task approval
project_router = APIRouter(prefix="/api/projects", tags=["tasks"])


class TaskCreateRequest(BaseModel):
"""Request model for creating a task."""
Expand Down Expand Up @@ -103,3 +110,135 @@ async def create_task(
except Exception as e:
logger.error(f"Error creating task: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Error creating task")


# ============================================================================
# Task Approval Models and Endpoint (Feature: 016-planning-phase-automation)
# ============================================================================


class TaskApprovalRequest(BaseModel):
"""Request model for task approval."""
approved: bool
excluded_task_ids: List[int] = Field(default_factory=list)


class TaskApprovalResponse(BaseModel):
"""Response model for task approval."""
success: bool
phase: str
approved_count: int
excluded_count: int
message: str


@project_router.post("/{project_id}/tasks/approve")
async def approve_tasks(
project_id: int,
request: TaskApprovalRequest,
db: Database = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> TaskApprovalResponse:
"""Approve tasks and transition project to development phase.

This endpoint allows users to approve generated tasks after reviewing them.
Approved tasks are updated to 'pending' status and the project phase
transitions to 'active' (development).

Args:
project_id: Project ID
request: Approval request with approved flag and optional exclusions
db: Database connection
current_user: Authenticated user

Returns:
TaskApprovalResponse with summary of approval

Raises:
HTTPException:
- 400: Project not in planning phase
- 403: Access denied
- 404: Project or tasks not found
"""
# Verify project exists
project = db.get_project(project_id)
if not project:
raise HTTPException(
status_code=404,
detail=f"Project {project_id} not found"
)

# Authorization check
if not db.user_has_project_access(current_user.id, project_id):
raise HTTPException(status_code=403, detail="Access denied")

# Check if user is rejecting
if not request.approved:
return TaskApprovalResponse(
success=False,
phase=project.get("phase", "planning"),
approved_count=0,
excluded_count=0,
message="Tasks were not approved. Please review and modify tasks before approving."
)

# Validate project is in planning phase
current_phase = project.get("phase", "discovery")
if current_phase != "planning":
raise HTTPException(
status_code=400,
detail=f"Project must be in planning phase to approve tasks. Current phase: {current_phase}"
)

# Get all tasks for the project
tasks = db.get_project_tasks(project_id)
if not tasks:
raise HTTPException(
status_code=404,
detail="No tasks found for this project. Generate tasks before approving."
)

# Separate approved and excluded tasks
# Note: Excluded tasks remain unchanged in the database for audit trail.
# They are not deleted or modified - users can re-include them later if needed.
excluded_ids = set(request.excluded_task_ids)
approved_tasks = [t for t in tasks if t.id not in excluded_ids]
excluded_tasks = [t for t in tasks if t.id in excluded_ids]

# Transition project phase to active FIRST (fails early before modifying tasks)
# This ensures we don't leave tasks in pending status if phase transition fails
try:
PhaseManager.transition(project_id, "active", db)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to transition phase for project {project_id}: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail="Failed to transition project to development phase"
)

# Update approved tasks to pending status (after phase transition succeeds)
for task in approved_tasks:
db.update_task(task.id, {"status": "pending"})
Comment on lines +204 to +223

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check TaskStatus enum definition and status patterns
echo "=== Searching for TaskStatus enum ==="
ast-grep --pattern 'class TaskStatus'

echo -e "\n=== Searching for status string definitions ==="
rg -n 'status.*=.*["\']' codeframe/ --type py -A2 | head -50

echo -e "\n=== Checking for excluded/rejected status handling ==="
rg -nP -C3 '\b(excluded|rejected|skip)\b' codeframe/ --type py | head -80

Repository: frankbria/codeframe

Length of output: 795


🏁 Script executed:

#!/bin/bash
# Check for transaction patterns and db.update_task usage
echo "=== Looking for transaction patterns ==="
rg -n 'transaction|atomic|begin|commit|rollback' codeframe/ --type py | head -30

echo -e "\n=== Check db.update_task implementation ==="
rg -n 'def update_task' codeframe/ --type py -A10

Repository: frankbria/codeframe

Length of output: 7292


🏁 Script executed:

#!/bin/bash
# Check the full approve_tasks function to understand context
echo "=== Finding approve_tasks function ==="
ast-grep --pattern 'def approve_tasks'

Repository: frankbria/codeframe

Length of output: 7500


🏁 Script executed:

#!/bin/bash
# Get full update_task implementation
echo "=== Full update_task implementation ==="
sed -n '97,130p' codeframe/persistence/repositories/task_repository.py

echo -e "\n=== Check how db uses SQLite connection ==="
rg -n 'self.conn.execute|cursor.execute' codeframe/persistence/repositories/task_repository.py | head -20

Repository: frankbria/codeframe

Length of output: 2033


🏁 Script executed:

#!/bin/bash
# Check if there's any error handling around task updates in approve_tasks or similar patterns
echo "=== Check for error handling patterns in task updates ==="
rg -B3 -A3 'for.*task.*in.*tasks' codeframe/ui/routers/tasks.py

echo -e "\n=== Check PhaseManager.transition to understand phase change ==="
ast-grep --pattern 'def transition'

Repository: frankbria/codeframe

Length of output: 2980


No action needed for excluded task handling; add error handling to task status update loop.

The approach of transitioning the project phase before updating tasks is correct (deliberate fail-fast pattern). However, two clarifications:

  1. Excluded tasks: No update is needed. The TaskStatus enum has no "excluded" or "rejected" status—only PENDING, ASSIGNED, IN_PROGRESS, BLOCKED, COMPLETED, and FAILED. Excluded tasks intentionally retain their original status; they're tracked only for broadcast and response counts.

  2. Task update loop (lines 220-221): The loop lacks error handling. If any update_task call fails, the project will be in "active" phase but some approved tasks won't have transitioned to "pending" status. Wrap the loop in a try-catch to handle failures gracefully or validate updateability before the loop.

🤖 Prompt for AI Agents
In @codeframe/ui/routers/tasks.py around lines 202 - 221, The approved_tasks
update loop lacks error handling so a failure in db.update_task can leave the
project phase as "active" while some tasks remain non-pending; wrap the for loop
that iterates over approved_tasks and calls db.update_task(task.id, {"status":
"pending"}) in a try/except block that logs the exception (include task.id and
project_id) and either rolls back or raises an HTTPException with a 500 status,
or pre-validate updateability by checking each task's current status against
TaskStatus (PENDING, ASSIGNED, IN_PROGRESS, BLOCKED, COMPLETED, FAILED) before
performing updates to avoid partial state changes.


# Broadcast development started event
await broadcast_development_started(
manager=manager,
project_id=project_id,
approved_count=len(approved_tasks),
excluded_count=len(excluded_tasks),
)

logger.info(
f"Tasks approved for project {project_id}: "
f"{len(approved_tasks)} approved, {len(excluded_tasks)} excluded"
)

return TaskApprovalResponse(
success=True,
phase="active",
approved_count=len(approved_tasks),
excluded_count=len(excluded_tasks),
message=f"Successfully approved {len(approved_tasks)} tasks. Development phase started."
)
1 change: 1 addition & 0 deletions codeframe/ui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ async def test_broadcast(message: dict, project_id: int = None):
app.include_router(review.router)
app.include_router(session.router)
app.include_router(tasks.router)
app.include_router(tasks.project_router)
app.include_router(websocket.router)
app.include_router(auth_router.router)

Expand Down
Loading
Loading