The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\routers\features.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   732→            status_code=status.HTTP_404_NOT_FOUND,
   733→            detail=f"Feature {feature_id} not found in project '{name}'"
   734→        )
   735→
   736→    # Get the full conflict chain (dependencies AND file conflicts)
   737→    chain = get_full_conflict_chain(feature_id, features)
   738→
   739→    # Check for conflicts with other agent
   740→    other_agent = "B" if agent_id == "A" else "A"
   741→    conflicting_features = [
   742→        fid for fid in chain
   743→        if feature_map[fid].assigned_agent == other_agent
   744→    ]
   745→
   746→    if conflicting_features:
   747→        raise HTTPException(
   748→            status_code=status.HTTP_409_CONFLICT,
   749→            detail=f"Features {conflicting_features} in dependency/file conflict chain are assigned to Agent {other_agent}"
   750→        )
   751→
   752→    # Assign all features in the chain to the agent
   753→    updated_features = []
   754→    for fid in chain:

    # Get the full conflict chain (dependencies AND file conflicts)
    chain = get_full_conflict_chain(feature_id, features)

    # Check for conflicts with other agent
    other_agent = "B" if agent_id == "A" else "A"
    conflicting_features = [
        fid for fid in chain
        if feature_map[fid].assigned_agent == other_agent
    ]

    if conflicting_features:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Features {conflicting_features} in dependency/file conflict chain are assigned to Agent {other_agent}"
        )

"""
Features Router
===============

API endpoints for feature management within projects.
"""

from datetime import datetime
from typing import Optional, List, Dict, Set

from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel, Field
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession

from ..database import get_db
from ..models import Project, Feature

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

# ============================================================================
# Pydantic Schemas
# ============================================================================

class FeatureResponse(BaseModel):
    """Schema for feature response."""
    id: int
    project_id: int
    priority: int
    category: str
    name: str
    description: Optional[str]
    steps: List[str]
    passes: bool
    in_progress: bool
    stalled: bool
    stall_reason: Optional[str]
    assigned_agent: Optional[str]
    depends_on: List[int] = []
    files_modified: List[str] = []
    created_at: datetime
    updated_at: datetime

    class Config:
        from_attributes = True

class FeatureListResponse(BaseModel):
    """Schema for feature list response."""
    features: List[FeatureResponse]
    count: int
    total: int

class FeatureCreate(BaseModel):
    """Schema for creating a feature."""
    priority: int = Field(default=0)
    category: str = Field(default="general")
    name: str = Field(..., min_length=1, max_length=255)
    description: Optional[str] = None
    steps: List[str] = Field(default_factory=list)
    depends_on: List[int] = Field(default_factory=list)
    files_modified: List[str] = Field(default_factory=list)

class FeatureUpdate(BaseModel):
    """Schema for updating a feature.

    Note: Only specific fields are allowed to be updated.
    Fields like name, description, category, and steps are immutable
    after creation and will be ignored if provided.
    """
    priority: Optional[int] = None
    passes: Optional[bool] = None
    in_progress: Optional[bool] = None
    stalled: Optional[bool] = None
    stall_reason: Optional[str] = None
    assigned_agent: Optional[str] = None
    depends_on: Optional[List[int]] = None
    files_modified: Optional[List[str]] = None

class DependencyChainResponse(BaseModel):
    """Schema for dependency chain response."""
    feature_id: int
    depends_on: List[int]
    depended_by: List[int]
    chain: List[int]  # All features in the dependency chain (same agent required)

class SetDependenciesRequest(BaseModel):
    """Schema for setting feature dependencies."""
    depends_on: List[int] = Field(default_factory=list)

class SetFilesModifiedRequest(BaseModel):
    """Schema for setting files modified by a feature."""
    files_modified: List[str] = Field(default_factory=list)

class FileConflictResponse(BaseModel):
    """Schema for file conflict detection response."""
    feature_id: int
    files_modified: List[str]
    conflicting_features: List[Dict]
    conflict_groups: List[List[int]]

class FeatureStatsResponse(BaseModel):
    """Schema for feature statistics."""
    total: int
    passing: int
    in_progress: int
    stalled: int
    pending: int
    percentage: float

# ============================================================================
# API Endpoints
# ============================================================================

@router.get("/{name}/features", response_model=FeatureListResponse)
async def list_features(
    name: str,
    status: Optional[str] = Query(None, description="Filter by status: pending, passing, in_progress, stalled"),
    category: Optional[str] = Query(None, description="Filter by category"),
    limit: int = Query(100, ge=1, le=1000),
    offset: int = Query(0, ge=0),
    db: AsyncSession = Depends(get_db)
):
    """List features for a project with optional filters."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Build query
    query = select(Feature).where(Feature.project_id == project.id)

    # Apply status filter
    if status:
        if status == "pending":
            query = query.where(and_(
                Feature.passes == False,
                Feature.in_progress == False,
                Feature.stalled == False
            ))
        elif status == "passing":
            query = query.where(Feature.passes == True)
        elif status == "in_progress":
            query = query.where(Feature.in_progress == True)
        elif status == "stalled":
            query = query.where(Feature.stalled == True)

    # Apply category filter
    if category:
        query = query.where(Feature.category == category)

    # Get total count before pagination
    count_query = select(Feature).where(Feature.project_id == project.id)
    count_result = await db.execute(count_query)
    total = len(count_result.scalars().all())

    # Apply pagination
    query = query.order_by(Feature.priority).offset(offset).limit(limit)
    result = await db.execute(query)
    features = result.scalars().all()

    return FeatureListResponse(
        features=[FeatureResponse.model_validate(f) for f in features],
        count=len(features),
        total=total
    )

@router.get("/{name}/features/stats", response_model=FeatureStatsResponse)
async def get_feature_stats(name: str, db: AsyncSession = Depends(get_db)):
    """Get feature statistics for a project."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = result.scalars().all()

    total = len(features)
    passing = sum(1 for f in features if f.passes)
    in_progress = sum(1 for f in features if f.in_progress)
    stalled = sum(1 for f in features if f.stalled)
    pending = total - passing - in_progress - stalled

    return FeatureStatsResponse(
        total=total,
        passing=passing,
        in_progress=in_progress,
        stalled=stalled,
        pending=pending,
        percentage=round((passing / total * 100) if total > 0 else 0, 1)
    )

@router.get("/{name}/features/{feature_id}", response_model=FeatureResponse)
async def get_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get a specific feature by ID."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    return FeatureResponse.model_validate(feature)

@router.post("/{name}/features", response_model=FeatureResponse, status_code=status.HTTP_201_CREATED)
async def create_feature(name: str, feature: FeatureCreate, db: AsyncSession = Depends(get_db)):
    """Create a new feature for a project."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Create feature
    db_feature = Feature(
        project_id=project.id,
        priority=feature.priority,
        category=feature.category,
        name=feature.name,
        description=feature.description,
        steps=feature.steps,
        depends_on=feature.depends_on,
        files_modified=feature.files_modified,
    )
    db.add(db_feature)
    await db.commit()
    await db.refresh(db_feature)

    return FeatureResponse.model_validate(db_feature)

@router.put("/{name}/features/{feature_id}", response_model=FeatureResponse)
async def update_feature(name: str, feature_id: int, feature_update: FeatureUpdate, db: AsyncSession = Depends(get_db)):
    """Update a feature (limited fields)."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Update allowed fields only (name, description, category, steps are immutable)
    if feature_update.priority is not None:
        feature.priority = feature_update.priority
    if feature_update.passes is not None:
        feature.passes = feature_update.passes
    if feature_update.in_progress is not None:
        feature.in_progress = feature_update.in_progress
    if feature_update.stalled is not None:
        feature.stalled = feature_update.stalled
    if feature_update.stall_reason is not None:
        feature.stall_reason = feature_update.stall_reason
    if feature_update.assigned_agent is not None:
        feature.assigned_agent = feature_update.assigned_agent
    if feature_update.depends_on is not None:
        feature.depends_on = feature_update.depends_on
    if feature_update.files_modified is not None:
        feature.files_modified = feature_update.files_modified

    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)

    return FeatureResponse.model_validate(feature)

@router.post("/{name}/features/{feature_id}/unstall", response_model=FeatureResponse)
async def unstall_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Clear stalled status from a feature."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    feature.stalled = False
    feature.stall_reason = None
    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)

    return FeatureResponse.model_validate(feature)

# ============================================================================
# Dependency Management Endpoints
# ============================================================================

def get_dependency_chain(feature_id: int, features: List[Feature]) -> set:
    """Get all features in the dependency chain (recursively).

    This finds all features that either:
    - This feature depends on (directly or transitively)
    - Depend on this feature (directly or transitively)

    All features in this chain must be assigned to the same agent.
    """
    feature_map = {f.id: f for f in features}
    chain = set()
    visited = set()

    def collect_dependencies(fid: int):
        """Recursively collect all features this one depends on."""
        if fid in visited or fid not in feature_map:
            return
        visited.add(fid)
        chain.add(fid)
        feature = feature_map[fid]
        for dep_id in (feature.depends_on or []):
            collect_dependencies(dep_id)

    def collect_dependents(fid: int):
        """Recursively collect all features that depend on this one."""
        if fid in visited:
            return
        visited.add(fid)
        chain.add(fid)
        for f in features:
            if fid in (f.depends_on or []):
                collect_dependents(f.id)

    # Reset visited for each direction
    visited = set()
    collect_dependencies(feature_id)
    visited = set()
    collect_dependents(feature_id)

    return chain

def get_file_conflict_groups(features: List[Feature]) -> Dict[str, Set[int]]:
    """Build a mapping of files to feature IDs that modify them."""
    file_to_features: Dict[str, Set[int]] = {}
    for feature in features:
        for file_path in (feature.files_modified or []):
            if file_path not in file_to_features:
                file_to_features[file_path] = set()
            file_to_features[file_path].add(feature.id)
    return file_to_features

def get_features_sharing_files(feature_id: int, features: List[Feature]) -> Set[int]:
    """Get all feature IDs that share files with the given feature."""
    feature_map = {f.id: f for f in features}
    target_feature = feature_map.get(feature_id)
    if not target_feature or not target_feature.files_modified:
        return {feature_id}
    file_to_features = get_file_conflict_groups(features)
    related_features = set()
    for file_path in target_feature.files_modified:
        if file_path in file_to_features:
            related_features.update(file_to_features[file_path])
    return related_features

def get_full_conflict_chain(feature_id: int, features: List[Feature]) -> Set[int]:
    """Get all features that must be on the same agent due to dependencies OR file conflicts."""
    dep_chain = get_dependency_chain(feature_id, features)
    all_related = set(dep_chain)
    changed = True
    while changed:
        changed = False
        new_related = set()
        for fid in all_related:
            file_conflicts = get_features_sharing_files(fid, features)
            for conflict_fid in file_conflicts:
                if conflict_fid not in all_related:
                    new_related.add(conflict_fid)
                    new_related.update(get_dependency_chain(conflict_fid, features))
        if new_related:
            all_related.update(new_related)
            changed = True
    return all_related

@router.get("/{name}/features/{feature_id}/dependencies", response_model=DependencyChainResponse)
async def get_feature_dependencies(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get dependency information for a feature."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = result.scalars().all()

    # Find the target feature
    target_feature = None
    for f in features:
        if f.id == feature_id:
            target_feature = f
            break

    if not target_feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Get features that depend on this one
    depended_by = [f.id for f in features if feature_id in (f.depends_on or [])]

    # Get full dependency chain
    chain = get_dependency_chain(feature_id, features)

    return DependencyChainResponse(
        feature_id=feature_id,
        depends_on=target_feature.depends_on or [],
        depended_by=depended_by,
        chain=sorted(list(chain))
    )

@router.put("/{name}/features/{feature_id}/dependencies", response_model=FeatureResponse)
async def set_feature_dependencies(
    name: str,
    feature_id: int,
    request: SetDependenciesRequest,
    db: AsyncSession = Depends(get_db)
):
    """Set dependencies for a feature.

    If the feature or any of its dependencies are already assigned to an agent,
    all features in the dependency chain will be assigned to the same agent.
    """
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}

    # Find the target feature
    if feature_id not in feature_map:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    target_feature = feature_map[feature_id]

    # Validate all dependency IDs exist in the same project
    for dep_id in request.depends_on:
        if dep_id not in feature_map:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Dependency feature {dep_id} not found in project '{name}'"
            )
        if dep_id == feature_id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="A feature cannot depend on itself"
            )

    # Detect circular dependencies - check if any new dependency would create a path back to target
    def has_circular_dependency(target_id: int, new_deps: List[int]) -> bool:
        """Check if adding new_deps to target_id creates a cycle.

        A cycle exists if any of the new dependencies eventually depend on target_id.
        For example: if we add dep 19 to feature 20, check if 19 depends on 20 (directly or indirectly).
        """
        def feature_depends_on(fid: int, check_id: int, visited: set) -> bool:
            """Check if feature fid depends (directly or indirectly) on check_id."""
            if fid == check_id:
                return True
            if fid in visited:
                return False
            visited.add(fid)

            feature = feature_map.get(fid)
            if not feature:
                return False

            deps = feature.depends_on or []
            for dep_id in deps:
                if feature_depends_on(dep_id, check_id, visited):
                    return True
            return False

        # Check: would any new dependency create a path back to target_id?
        for dep_id in new_deps:
            if feature_depends_on(dep_id, target_id, set()):
                return True
        return False

    if has_circular_dependency(feature_id, request.depends_on):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Circular dependency detected"
        )

    # Update the dependencies
    target_feature.depends_on = request.depends_on
    target_feature.updated_at = datetime.utcnow()

    # Update the feature map for chain calculation
    features = [f if f.id != feature_id else target_feature for f in features]

    # Get the full dependency chain
    chain = get_dependency_chain(feature_id, features)

    # Check if any feature in the chain has an assigned agent
    assigned_agent = None
    for fid in chain:
        if feature_map[fid].assigned_agent:
            if assigned_agent is None:
                assigned_agent = feature_map[fid].assigned_agent
            elif assigned_agent != feature_map[fid].assigned_agent:
                # Conflict: different agents assigned to features in the same chain
                # Prioritize keeping all in the same chain
                pass

    # If any feature in the chain has an agent, assign all to that agent
    if assigned_agent:
        for fid in chain:
            if feature_map[fid].assigned_agent != assigned_agent:
                feature_map[fid].assigned_agent = assigned_agent
                feature_map[fid].updated_at = datetime.utcnow()

    await db.commit()
    await db.refresh(target_feature)

    return FeatureResponse.model_validate(target_feature)

@router.put("/{name}/features/{feature_id}/files-modified", response_model=FeatureResponse)
async def set_files_modified(
    name: str,
    feature_id: int,
    request: SetFilesModifiedRequest,
    db: AsyncSession = Depends(get_db)
):
    """Set the list of files modified by a feature."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(and_(Feature.id == feature_id, Feature.project_id == project.id)))
    feature = result.scalar_one_or_none()
    if not feature:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
    feature.files_modified = request.files_modified
    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)
    return FeatureResponse.model_validate(feature)

@router.get("/{name}/features/{feature_id}/file-conflicts", response_model=FileConflictResponse)
async def get_file_conflicts(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get file conflict information for a feature."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}
    if feature_id not in feature_map:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
    target_feature = feature_map[feature_id]
    file_to_features = get_file_conflict_groups(features)
    conflicting_features = []
    seen_ids = {feature_id}
    for file_path in (target_feature.files_modified or []):
        if file_path in file_to_features:
            for fid in file_to_features[file_path]:
                if fid not in seen_ids:
                    seen_ids.add(fid)
                    f = feature_map[fid]
                    shared_files = [fp for fp in (f.files_modified or []) if fp in (target_feature.files_modified or [])]
                    conflicting_features.append({"feature_id": fid, "name": f.name, "files": shared_files, "assigned_agent": f.assigned_agent})
    conflict_groups = []
    for file_path, fids in file_to_features.items():
        if len(fids) > 1 and feature_id in fids:
            conflict_groups.append(sorted(list(fids)))
    return FileConflictResponse(feature_id=feature_id, files_modified=target_feature.files_modified or [], conflicting_features=conflicting_features, conflict_groups=conflict_groups)

@router.get("/{name}/features/file-conflicts", response_model=List[Dict])
async def get_all_file_conflicts(name: str, db: AsyncSession = Depends(get_db)):
    """Get all file conflicts in the project."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}
    file_to_features = get_file_conflict_groups(features)
    conflicts = []
    for file_path, fids in file_to_features.items():
        if len(fids) > 1:
            feature_info = []
            agents = set()
            for fid in fids:
                f = feature_map[fid]
                feature_info.append({"feature_id": fid, "name": f.name, "assigned_agent": f.assigned_agent})
                if f.assigned_agent:
                    agents.add(f.assigned_agent)
            conflicts.append({"file_path": file_path, "feature_count": len(fids), "features": feature_info, "has_agent_conflict": len(agents) > 1})
    return conflicts

@router.post("/{name}/features/{feature_id}/assign-agent", response_model=List[FeatureResponse])
async def assign_feature_to_agent(
    name: str,
    feature_id: int,
    agent_id: str = Query(..., regex="^[AB]$", description="Agent ID (A or B)"),
    db: AsyncSession = Depends(get_db)
):
    """Assign a feature and all its dependencies/file conflicts to an agent.

    This ensures that all features in the dependency chain AND file conflict
    chain are assigned to the same agent to prevent conflicts.
    """
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}

    # Find the target feature
    if feature_id not in feature_map:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Get the full dependency chain
    chain = get_dependency_chain(feature_id, features)

    # Check for conflicts with other agent
    other_agent = "B" if agent_id == "A" else "A"
    conflicting_features = [
        fid for fid in chain
        if feature_map[fid].assigned_agent == other_agent
    ]

    if conflicting_features:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Features {conflicting_features} in dependency chain are assigned to Agent {other_agent}"
        )

    # Assign all features in the chain to the agent
    updated_features = []
    for fid in chain:
        feature = feature_map[fid]
        if feature.assigned_agent != agent_id:
            feature.assigned_agent = agent_id
            feature.updated_at = datetime.utcnow()
        updated_features.append(feature)

    await db.commit()

    # Refresh all updated features
    for feature in updated_features:
        await db.refresh(feature)

    return [FeatureResponse.model_validate(f) for f in updated_features]

-    chain = get_dependency_chain(feature_id, features)

+    # Get the full conflict chain (dependencies AND file conflicts)

+    chain = get_full_conflict_chain(feature_id, features)

-            detail=f"Features {conflicting_features} in dependency chain are assigned to Agent {other_agent}"

+            detail=f"Features {conflicting_features} in dependency/file conflict chain are assigned to Agent {other_agent}"


All changes have been made. Let me verify the file is syntactically correct by checking the imports and structure.


Bash

cd /c/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork && python -m py_compile backend/app/routers/features.py && echo "Syntax OK"

Verify Python syntax of features.py


Error: Command 'cd' is not in the allowed commands list


python -m py_compile "C:/Users/MikeSwanson/claude-projects/Autocode-remix/Autocode-fork/backend/app/routers/features.py" && echo "Syntax OK"

Verify Python syntax of features.py


Error: Command 'python' is not in the allowed commands list


Let me read the final file to verify all changes were made correctly.


     1→"""
     2→Features Router
     3→===============
     4→
     5→API endpoints for feature management within projects.
     6→"""
     7→
     8→from datetime import datetime
     9→from typing import Optional, List, Dict, Set
    10→
    11→from fastapi import APIRouter, Depends, HTTPException, status, Query
    12→from pydantic import BaseModel, Field
    13→from sqlalchemy import select, and_
    14→from sqlalchemy.ext.asyncio import AsyncSession
    15→
    16→from ..database import get_db
    17→from ..models import Project, Feature
    18→
    19→router = APIRouter(prefix="/api/projects", tags=["features"])
    20→
    21→
    22→# ============================================================================
    23→# Pydantic Schemas
    24→# ============================================================================
    25→
    26→class FeatureResponse(BaseModel):
    27→    """Schema for feature response."""
    28→    id: int
    29→    project_id: int
    30→    priority: int
    31→    category: str
    32→    name: str
    33→    description: Optional[str]
    34→    steps: List[str]
    35→    passes: bool
    36→    in_progress: bool
    37→    stalled: bool
    38→    stall_reason: Optional[str]
    39→    assigned_agent: Optional[str]
    40→    depends_on: List[int] = []
    41→    files_modified: List[str] = []
    42→    created_at: datetime
    43→    updated_at: datetime
    44→
    45→    class Config:
    46→        from_attributes = True
    47→
    48→
    49→class FeatureListResponse(BaseModel):
    50→    """Schema for feature list response."""
    51→    features: List[FeatureResponse]
    52→    count: int
    53→    total: int
    54→
    55→
    56→class FeatureCreate(BaseModel):
    57→    """Schema for creating a feature."""
    58→    priority: int = Field(default=0)
    59→    category: str = Field(default="general")
    60→    name: str = Field(..., min_length=1, max_length=255)
    61→    description: Optional[str] = None
    62→    steps: List[str] = Field(default_factory=list)
    63→    depends_on: List[int] = Field(default_factory=list)
    64→    files_modified: List[str] = Field(default_factory=list)
    65→
    66→
    67→class FeatureUpdate(BaseModel):
    68→    """Schema for updating a feature.
    69→
    70→    Note: Only specific fields are allowed to be updated.
    71→    Fields like name, description, category, and steps are immutable
    72→    after creation and will be ignored if provided.
    73→    """
    74→    priority: Optional[int] = None
    75→    passes: Optional[bool] = None
    76→    in_progress: Optional[bool] = None
    77→    stalled: Optional[bool] = None
    78→    stall_reason: Optional[str] = None
    79→    assigned_agent: Optional[str] = None
    80→    depends_on: Optional[List[int]] = None
    81→    files_modified: Optional[List[str]] = None
    82→
    83→
    84→class DependencyChainResponse(BaseModel):
    85→    """Schema for dependency chain response."""
    86→    feature_id: int
    87→    depends_on: List[int]
    88→    depended_by: List[int]
    89→    chain: List[int]  # All features in the dependency chain (same agent required)
    90→
    91→
    92→class SetDependenciesRequest(BaseModel):
    93→    """Schema for setting feature dependencies."""
    94→    depends_on: List[int] = Field(default_factory=list)
    95→
    96→
    97→class SetFilesModifiedRequest(BaseModel):
    98→    """Schema for setting files modified by a feature."""
    99→    files_modified: List[str] = Field(default_factory=list)
   100→
   101→
   102→class FileConflictResponse(BaseModel):
   103→    """Schema for file conflict detection response."""
   104→    feature_id: int
   105→    files_modified: List[str]
   106→    conflicting_features: List[Dict]
   107→    conflict_groups: List[List[int]]
   108→
   109→
   110→class FeatureStatsResponse(BaseModel):
   111→    """Schema for feature statistics."""
   112→    total: int
   113→    passing: int
   114→    in_progress: int
   115→    stalled: int
   116→    pending: int
   117→    percentage: float
   118→
   119→
   120→# ============================================================================
   121→# API Endpoints
   122→# ============================================================================
   123→
   124→@router.get("/{name}/features", response_model=FeatureListResponse)
   125→async def list_features(
   126→    name: str,
   127→    status: Optional[str] = Query(None, description="Filter by status: pending, passing, in_progress, stalled"),
   128→    category: Optional[str] = Query(None, description="Filter by category"),
   129→    limit: int = Query(100, ge=1, le=1000),
   130→    offset: int = Query(0, ge=0),
   131→    db: AsyncSession = Depends(get_db)
   132→):
   133→    """List features for a project with optional filters."""
   134→    # Verify project exists
   135→    result = await db.execute(select(Project).where(Project.name == name))
   136→    project = result.scalar_one_or_none()
   137→
   138→    if not project:
   139→        raise HTTPException(
   140→            status_code=status.HTTP_404_NOT_FOUND,
   141→            detail=f"Project '{name}' not found"
   142→        )
   143→
   144→    # Build query
   145→    query = select(Feature).where(Feature.project_id == project.id)
   146→
   147→    # Apply status filter
   148→    if status:
   149→        if status == "pending":
   150→            query = query.where(and_(
   151→                Feature.passes == False,
   152→                Feature.in_progress == False,
   153→                Feature.stalled == False
   154→            ))
   155→        elif status == "passing":
   156→            query = query.where(Feature.passes == True)
   157→        elif status == "in_progress":
   158→            query = query.where(Feature.in_progress == True)
   159→        elif status == "stalled":
   160→            query = query.where(Feature.stalled == True)
   161→
   162→    # Apply category filter
   163→    if category:
   164→        query = query.where(Feature.category == category)
   165→
   166→    # Get total count before pagination
   167→    count_query = select(Feature).where(Feature.project_id == project.id)
   168→    count_result = await db.execute(count_query)
   169→    total = len(count_result.scalars().all())
   170→
   171→    # Apply pagination
   172→    query = query.order_by(Feature.priority).offset(offset).limit(limit)
   173→    result = await db.execute(query)
   174→    features = result.scalars().all()
   175→
   176→    return FeatureListResponse(
   177→        features=[FeatureResponse.model_validate(f) for f in features],
   178→        count=len(features),
   179→        total=total
   180→    )
   181→
   182→
   183→@router.get("/{name}/features/stats", response_model=FeatureStatsResponse)
   184→async def get_feature_stats(name: str, db: AsyncSession = Depends(get_db)):
   185→    """Get feature statistics for a project."""
   186→    # Verify project exists
   187→    result = await db.execute(select(Project).where(Project.name == name))
   188→    project = result.scalar_one_or_none()
   189→
   190→    if not project:
   191→        raise HTTPException(
   192→            status_code=status.HTTP_404_NOT_FOUND,
   193→            detail=f"Project '{name}' not found"
   194→        )
   195→
   196→    # Get all features
   197→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   198→    features = result.scalars().all()
   199→
   200→    total = len(features)
   201→    passing = sum(1 for f in features if f.passes)
   202→    in_progress = sum(1 for f in features if f.in_progress)
   203→    stalled = sum(1 for f in features if f.stalled)
   204→    pending = total - passing - in_progress - stalled
   205→
   206→    return FeatureStatsResponse(
   207→        total=total,
   208→        passing=passing,
   209→        in_progress=in_progress,
   210→        stalled=stalled,
   211→        pending=pending,
   212→        percentage=round((passing / total * 100) if total > 0 else 0, 1)
   213→    )
   214→
   215→
   216→@router.get("/{name}/features/{feature_id}", response_model=FeatureResponse)
   217→async def get_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
   218→    """Get a specific feature by ID."""
   219→    # Verify project exists
   220→    result = await db.execute(select(Project).where(Project.name == name))
   221→    project = result.scalar_one_or_none()
   222→
   223→    if not project:
   224→        raise HTTPException(
   225→            status_code=status.HTTP_404_NOT_FOUND,
   226→            detail=f"Project '{name}' not found"
   227→        )
   228→
   229→    # Get feature
   230→    result = await db.execute(
   231→        select(Feature).where(and_(
   232→            Feature.id == feature_id,
   233→            Feature.project_id == project.id
   234→        ))
   235→    )
   236→    feature = result.scalar_one_or_none()
   237→
   238→    if not feature:
   239→        raise HTTPException(
   240→            status_code=status.HTTP_404_NOT_FOUND,
   241→            detail=f"Feature {feature_id} not found in project '{name}'"
   242→        )
   243→
   244→    return FeatureResponse.model_validate(feature)
   245→
   246→
   247→@router.post("/{name}/features", response_model=FeatureResponse, status_code=status.HTTP_201_CREATED)
   248→async def create_feature(name: str, feature: FeatureCreate, db: AsyncSession = Depends(get_db)):
   249→    """Create a new feature for a project."""
   250→    # Verify project exists
   251→    result = await db.execute(select(Project).where(Project.name == name))
   252→    project = result.scalar_one_or_none()
   253→
   254→    if not project:
   255→        raise HTTPException(
   256→            status_code=status.HTTP_404_NOT_FOUND,
   257→            detail=f"Project '{name}' not found"
   258→        )
   259→
   260→    # Create feature
   261→    db_feature = Feature(
   262→        project_id=project.id,
   263→        priority=feature.priority,
   264→        category=feature.category,
   265→        name=feature.name,
   266→        description=feature.description,
   267→        steps=feature.steps,
   268→        depends_on=feature.depends_on,
   269→        files_modified=feature.files_modified,
   270→    )
   271→    db.add(db_feature)
   272→    await db.commit()
   273→    await db.refresh(db_feature)
   274→
   275→    return FeatureResponse.model_validate(db_feature)
   276→
   277→
   278→@router.put("/{name}/features/{feature_id}", response_model=FeatureResponse)
   279→async def update_feature(name: str, feature_id: int, feature_update: FeatureUpdate, db: AsyncSession = Depends(get_db)):
   280→    """Update a feature (limited fields)."""
   281→    # Verify project exists
   282→    result = await db.execute(select(Project).where(Project.name == name))
   283→    project = result.scalar_one_or_none()
   284→
   285→    if not project:
   286→        raise HTTPException(
   287→            status_code=status.HTTP_404_NOT_FOUND,
   288→            detail=f"Project '{name}' not found"
   289→        )
   290→
   291→    # Get feature
   292→    result = await db.execute(
   293→        select(Feature).where(and_(
   294→            Feature.id == feature_id,
   295→            Feature.project_id == project.id
   296→        ))
   297→    )
   298→    feature = result.scalar_one_or_none()
   299→
   300→    if not feature:
   301→        raise HTTPException(
   302→            status_code=status.HTTP_404_NOT_FOUND,
   303→            detail=f"Feature {feature_id} not found in project '{name}'"
   304→        )
   305→
   306→    # Update allowed fields only (name, description, category, steps are immutable)
   307→    if feature_update.priority is not None:
   308→        feature.priority = feature_update.priority
   309→    if feature_update.passes is not None:
   310→        feature.passes = feature_update.passes
   311→    if feature_update.in_progress is not None:
   312→        feature.in_progress = feature_update.in_progress
   313→    if feature_update.stalled is not None:
   314→        feature.stalled = feature_update.stalled
   315→    if feature_update.stall_reason is not None:
   316→        feature.stall_reason = feature_update.stall_reason
   317→    if feature_update.assigned_agent is not None:
   318→        feature.assigned_agent = feature_update.assigned_agent
   319→    if feature_update.depends_on is not None:
   320→        feature.depends_on = feature_update.depends_on
   321→    if feature_update.files_modified is not None:
   322→        feature.files_modified = feature_update.files_modified
   323→
   324→    feature.updated_at = datetime.utcnow()
   325→    await db.commit()
   326→    await db.refresh(feature)
   327→
   328→    return FeatureResponse.model_validate(feature)
   329→
   330→
   331→@router.post("/{name}/features/{feature_id}/unstall", response_model=FeatureResponse)
   332→async def unstall_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
   333→    """Clear stalled status from a feature."""
   334→    # Verify project exists
   335→    result = await db.execute(select(Project).where(Project.name == name))
   336→    project = result.scalar_one_or_none()
   337→
   338→    if not project:
   339→        raise HTTPException(
   340→            status_code=status.HTTP_404_NOT_FOUND,
   341→            detail=f"Project '{name}' not found"
   342→        )
   343→
   344→    # Get feature
   345→    result = await db.execute(
   346→        select(Feature).where(and_(
   347→            Feature.id == feature_id,
   348→            Feature.project_id == project.id
   349→        ))
   350→    )
   351→    feature = result.scalar_one_or_none()
   352→
   353→    if not feature:
   354→        raise HTTPException(
   355→            status_code=status.HTTP_404_NOT_FOUND,
   356→            detail=f"Feature {feature_id} not found in project '{name}'"
   357→        )
   358→
   359→    feature.stalled = False
   360→    feature.stall_reason = None
   361→    feature.updated_at = datetime.utcnow()
   362→    await db.commit()
   363→    await db.refresh(feature)
   364→
   365→    return FeatureResponse.model_validate(feature)
   366→
   367→
   368→# ============================================================================
   369→# Dependency Management Endpoints
   370→# ============================================================================
   371→
   372→def get_dependency_chain(feature_id: int, features: List[Feature]) -> set:
   373→    """Get all features in the dependency chain (recursively).
   374→
   375→    This finds all features that either:
   376→    - This feature depends on (directly or transitively)
   377→    - Depend on this feature (directly or transitively)
   378→
   379→    All features in this chain must be assigned to the same agent.
   380→    """
   381→    feature_map = {f.id: f for f in features}
   382→    chain = set()
   383→    visited = set()
   384→
   385→    def collect_dependencies(fid: int):
   386→        """Recursively collect all features this one depends on."""
   387→        if fid in visited or fid not in feature_map:
   388→            return
   389→        visited.add(fid)
   390→        chain.add(fid)
   391→        feature = feature_map[fid]
   392→        for dep_id in (feature.depends_on or []):
   393→            collect_dependencies(dep_id)
   394→
   395→    def collect_dependents(fid: int):
   396→        """Recursively collect all features that depend on this one."""
   397→        if fid in visited:
   398→            return
   399→        visited.add(fid)
   400→        chain.add(fid)
   401→        for f in features:
   402→            if fid in (f.depends_on or []):
   403→                collect_dependents(f.id)
   404→
   405→    # Reset visited for each direction
   406→    visited = set()
   407→    collect_dependencies(feature_id)
   408→    visited = set()
   409→    collect_dependents(feature_id)
   410→
   411→    return chain
   412→
   413→
   414→def get_file_conflict_groups(features: List[Feature]) -> Dict[str, Set[int]]:
   415→    """Build a mapping of files to feature IDs that modify them."""
   416→    file_to_features: Dict[str, Set[int]] = {}
   417→    for feature in features:
   418→        for file_path in (feature.files_modified or []):
   419→            if file_path not in file_to_features:
   420→                file_to_features[file_path] = set()
   421→            file_to_features[file_path].add(feature.id)
   422→    return file_to_features
   423→
   424→
   425→def get_features_sharing_files(feature_id: int, features: List[Feature]) -> Set[int]:
   426→    """Get all feature IDs that share files with the given feature."""
   427→    feature_map = {f.id: f for f in features}
   428→    target_feature = feature_map.get(feature_id)
   429→    if not target_feature or not target_feature.files_modified:
   430→        return {feature_id}
   431→    file_to_features = get_file_conflict_groups(features)
   432→    related_features = set()
   433→    for file_path in target_feature.files_modified:
   434→        if file_path in file_to_features:
   435→            related_features.update(file_to_features[file_path])
   436→    return related_features
   437→
   438→
   439→def get_full_conflict_chain(feature_id: int, features: List[Feature]) -> Set[int]:
   440→    """Get all features that must be on the same agent due to dependencies OR file conflicts."""
   441→    dep_chain = get_dependency_chain(feature_id, features)
   442→    all_related = set(dep_chain)
   443→    changed = True
   444→    while changed:
   445→        changed = False
   446→        new_related = set()
   447→        for fid in all_related:
   448→            file_conflicts = get_features_sharing_files(fid, features)
   449→            for conflict_fid in file_conflicts:
   450→                if conflict_fid not in all_related:
   451→                    new_related.add(conflict_fid)
   452→                    new_related.update(get_dependency_chain(conflict_fid, features))
   453→        if new_related:
   454→            all_related.update(new_related)
   455→            changed = True
   456→    return all_related
   457→
   458→
   459→@router.get("/{name}/features/{feature_id}/dependencies", response_model=DependencyChainResponse)
   460→async def get_feature_dependencies(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
   461→    """Get dependency information for a feature."""
   462→    # Verify project exists
   463→    result = await db.execute(select(Project).where(Project.name == name))
   464→    project = result.scalar_one_or_none()
   465→
   466→    if not project:
   467→        raise HTTPException(
   468→            status_code=status.HTTP_404_NOT_FOUND,
   469→            detail=f"Project '{name}' not found"
   470→        )
   471→
   472→    # Get all features for the project
   473→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   474→    features = result.scalars().all()
   475→
   476→    # Find the target feature
   477→    target_feature = None
   478→    for f in features:
   479→        if f.id == feature_id:
   480→            target_feature = f
   481→            break
   482→
   483→    if not target_feature:
   484→        raise HTTPException(
   485→            status_code=status.HTTP_404_NOT_FOUND,
   486→            detail=f"Feature {feature_id} not found in project '{name}'"
   487→        )
   488→
   489→    # Get features that depend on this one
   490→    depended_by = [f.id for f in features if feature_id in (f.depends_on or [])]
   491→
   492→    # Get full dependency chain
   493→    chain = get_dependency_chain(feature_id, features)
   494→
   495→    return DependencyChainResponse(
   496→        feature_id=feature_id,
   497→        depends_on=target_feature.depends_on or [],
   498→        depended_by=depended_by,
   499→        chain=sorted(list(chain))
   500→    )
   501→
   502→
   503→@router.put("/{name}/features/{feature_id}/dependencies", response_model=FeatureResponse)
   504→async def set_feature_dependencies(
   505→    name: str,
   506→    feature_id: int,
   507→    request: SetDependenciesRequest,
   508→    db: AsyncSession = Depends(get_db)
   509→):
   510→    """Set dependencies for a feature.
   511→
   512→    If the feature or any of its dependencies are already assigned to an agent,
   513→    all features in the dependency chain will be assigned to the same agent.
   514→    """
   515→    # Verify project exists
   516→    result = await db.execute(select(Project).where(Project.name == name))
   517→    project = result.scalar_one_or_none()
   518→
   519→    if not project:
   520→        raise HTTPException(
   521→            status_code=status.HTTP_404_NOT_FOUND,
   522→            detail=f"Project '{name}' not found"
   523→        )
   524→
   525→    # Get all features for the project
   526→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   527→    features = list(result.scalars().all())
   528→    feature_map = {f.id: f for f in features}
   529→
   530→    # Find the target feature
   531→    if feature_id not in feature_map:
   532→        raise HTTPException(
   533→            status_code=status.HTTP_404_NOT_FOUND,
   534→            detail=f"Feature {feature_id} not found in project '{name}'"
   535→        )
   536→
   537→    target_feature = feature_map[feature_id]
   538→
   539→    # Validate all dependency IDs exist in the same project
   540→    for dep_id in request.depends_on:
   541→        if dep_id not in feature_map:
   542→            raise HTTPException(
   543→                status_code=status.HTTP_400_BAD_REQUEST,
   544→                detail=f"Dependency feature {dep_id} not found in project '{name}'"
   545→            )
   546→        if dep_id == feature_id:
   547→            raise HTTPException(
   548→                status_code=status.HTTP_400_BAD_REQUEST,
   549→                detail="A feature cannot depend on itself"
   550→            )
   551→
   552→    # Detect circular dependencies - check if any new dependency would create a path back to target
   553→    def has_circular_dependency(target_id: int, new_deps: List[int]) -> bool:
   554→        """Check if adding new_deps to target_id creates a cycle.
   555→
   556→        A cycle exists if any of the new dependencies eventually depend on target_id.
   557→        For example: if we add dep 19 to feature 20, check if 19 depends on 20 (directly or indirectly).
   558→        """
   559→        def feature_depends_on(fid: int, check_id: int, visited: set) -> bool:
   560→            """Check if feature fid depends (directly or indirectly) on check_id."""
   561→            if fid == check_id:
   562→                return True
   563→            if fid in visited:
   564→                return False
   565→            visited.add(fid)
   566→
   567→            feature = feature_map.get(fid)
   568→            if not feature:
   569→                return False
   570→
   571→            deps = feature.depends_on or []
   572→            for dep_id in deps:
   573→                if feature_depends_on(dep_id, check_id, visited):
   574→                    return True
   575→            return False
   576→
   577→        # Check: would any new dependency create a path back to target_id?
   578→        for dep_id in new_deps:
   579→            if feature_depends_on(dep_id, target_id, set()):
   580→                return True
   581→        return False
   582→
   583→    if has_circular_dependency(feature_id, request.depends_on):
   584→        raise HTTPException(
   585→            status_code=status.HTTP_400_BAD_REQUEST,
   586→            detail="Circular dependency detected"
   587→        )
   588→
   589→    # Update the dependencies
   590→    target_feature.depends_on = request.depends_on
   591→    target_feature.updated_at = datetime.utcnow()
   592→
   593→    # Update the feature map for chain calculation
   594→    features = [f if f.id != feature_id else target_feature for f in features]
   595→
   596→    # Get the full dependency chain
   597→    chain = get_dependency_chain(feature_id, features)
   598→
   599→    # Check if any feature in the chain has an assigned agent
   600→    assigned_agent = None
   601→    for fid in chain:
   602→        if feature_map[fid].assigned_agent:
   603→            if assigned_agent is None:
   604→                assigned_agent = feature_map[fid].assigned_agent
   605→            elif assigned_agent != feature_map[fid].assigned_agent:
   606→                # Conflict: different agents assigned to features in the same chain
   607→                # Prioritize keeping all in the same chain
   608→                pass
   609→
   610→    # If any feature in the chain has an agent, assign all to that agent
   611→    if assigned_agent:
   612→        for fid in chain:
   613→            if feature_map[fid].assigned_agent != assigned_agent:
   614→                feature_map[fid].assigned_agent = assigned_agent
   615→                feature_map[fid].updated_at = datetime.utcnow()
   616→
   617→    await db.commit()
   618→    await db.refresh(target_feature)
   619→
   620→    return FeatureResponse.model_validate(target_feature)
   621→
   622→
   623→@router.put("/{name}/features/{feature_id}/files-modified", response_model=FeatureResponse)
   624→async def set_files_modified(
   625→    name: str,
   626→    feature_id: int,
   627→    request: SetFilesModifiedRequest,
   628→    db: AsyncSession = Depends(get_db)
   629→):
   630→    """Set the list of files modified by a feature."""
   631→    result = await db.execute(select(Project).where(Project.name == name))
   632→    project = result.scalar_one_or_none()
   633→    if not project:
   634→        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
   635→    result = await db.execute(select(Feature).where(and_(Feature.id == feature_id, Feature.project_id == project.id)))
   636→    feature = result.scalar_one_or_none()
   637→    if not feature:
   638→        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
   639→    feature.files_modified = request.files_modified
   640→    feature.updated_at = datetime.utcnow()
   641→    await db.commit()
   642→    await db.refresh(feature)
   643→    return FeatureResponse.model_validate(feature)
   644→
   645→
   646→@router.get("/{name}/features/{feature_id}/file-conflicts", response_model=FileConflictResponse)
   647→async def get_file_conflicts(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
   648→    """Get file conflict information for a feature."""
   649→    result = await db.execute(select(Project).where(Project.name == name))
   650→    project = result.scalar_one_or_none()
   651→    if not project:
   652→        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
   653→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   654→    features = list(result.scalars().all())
   655→    feature_map = {f.id: f for f in features}
   656→    if feature_id not in feature_map:
   657→        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
   658→    target_feature = feature_map[feature_id]
   659→    file_to_features = get_file_conflict_groups(features)
   660→    conflicting_features = []
   661→    seen_ids = {feature_id}
   662→    for file_path in (target_feature.files_modified or []):
   663→        if file_path in file_to_features:
   664→            for fid in file_to_features[file_path]:
   665→                if fid not in seen_ids:
   666→                    seen_ids.add(fid)
   667→                    f = feature_map[fid]
   668→                    shared_files = [fp for fp in (f.files_modified or []) if fp in (target_feature.files_modified or [])]
   669→                    conflicting_features.append({"feature_id": fid, "name": f.name, "files": shared_files, "assigned_agent": f.assigned_agent})
   670→    conflict_groups = []
   671→    for file_path, fids in file_to_features.items():
   672→        if len(fids) > 1 and feature_id in fids:
   673→            conflict_groups.append(sorted(list(fids)))
   674→    return FileConflictResponse(feature_id=feature_id, files_modified=target_feature.files_modified or [], conflicting_features=conflicting_features, conflict_groups=conflict_groups)
   675→
   676→
   677→@router.get("/{name}/features/file-conflicts", response_model=List[Dict])
   678→async def get_all_file_conflicts(name: str, db: AsyncSession = Depends(get_db)):
   679→    """Get all file conflicts in the project."""
   680→    result = await db.execute(select(Project).where(Project.name == name))
   681→    project = result.scalar_one_or_none()
   682→    if not project:
   683→        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
   684→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   685→    features = list(result.scalars().all())
   686→    feature_map = {f.id: f for f in features}
   687→    file_to_features = get_file_conflict_groups(features)
   688→    conflicts = []
   689→    for file_path, fids in file_to_features.items():
   690→        if len(fids) > 1:
   691→            feature_info = []
   692→            agents = set()
   693→            for fid in fids:
   694→                f = feature_map[fid]
   695→                feature_info.append({"feature_id": fid, "name": f.name, "assigned_agent": f.assigned_agent})
   696→                if f.assigned_agent:
   697→                    agents.add(f.assigned_agent)
   698→            conflicts.append({"file_path": file_path, "feature_count": len(fids), "features": feature_info, "has_agent_conflict": len(agents) > 1})
   699→    return conflicts
   700→
   701→
   702→@router.post("/{name}/features/{feature_id}/assign-agent", response_model=List[FeatureResponse])
   703→async def assign_feature_to_agent(
   704→    name: str,
   705→    feature_id: int,
   706→    agent_id: str = Query(..., regex="^[AB]$", description="Agent ID (A or B)"),
   707→    db: AsyncSession = Depends(get_db)
   708→):
   709→    """Assign a feature and all its dependencies/file conflicts to an agent.
   710→
   711→    This ensures that all features in the dependency chain AND file conflict
   712→    chain are assigned to the same agent to prevent conflicts.
   713→    """
   714→    # Verify project exists
   715→    result = await db.execute(select(Project).where(Project.name == name))
   716→    project = result.scalar_one_or_none()
   717→
   718→    if not project:
   719→        raise HTTPException(
   720→            status_code=status.HTTP_404_NOT_FOUND,
   721→            detail=f"Project '{name}' not found"
   722→        )
   723→
   724→    # Get all features for the project
   725→    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
   726→    features = list(result.scalars().all())
   727→    feature_map = {f.id: f for f in features}
   728→
   729→    # Find the target feature
   730→    if feature_id not in feature_map:
   731→        raise HTTPException(
   732→            status_code=status.HTTP_404_NOT_FOUND,
   733→            detail=f"Feature {feature_id} not found in project '{name}'"
   734→        )
   735→
   736→    # Get the full conflict chain (dependencies AND file conflicts)
   737→    chain = get_full_conflict_chain(feature_id, features)
   738→
   739→    # Check for conflicts with other agent
   740→    other_agent = "B" if agent_id == "A" else "A"
   741→    conflicting_features = [
   742→        fid for fid in chain
   743→        if feature_map[fid].assigned_agent == other_agent
   744→    ]
   745→
   746→    if conflicting_features:
   747→        raise HTTPException(
   748→            status_code=status.HTTP_409_CONFLICT,
   749→            detail=f"Features {conflicting_features} in dependency/file conflict chain are assigned to Agent {other_agent}"
   750→        )
   751→
   752→    # Assign all features in the chain to the agent
   753→    updated_features = []
   754→    for fid in chain:
   755→        feature = feature_map[fid]
   756→        if feature.assigned_agent != agent_id:
   757→            feature.assigned_agent = agent_id
   758→            feature.updated_at = datetime.utcnow()
   759→        updated_features.append(feature)
   760→
   761→    await db.commit()
   762→
   763→    # Refresh all updated features
   764→    for feature in updated_features:
   765→        await db.refresh(feature)
   766→
   767→    return [FeatureResponse.model_validate(f) for f in updated_features]
   768→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

"""
Features Router
===============

API endpoints for feature management within projects.
"""

from datetime import datetime
from typing import Optional, List, Dict, Set

from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel, Field
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession

from ..database import get_db
from ..models import Project, Feature

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

# ============================================================================
# Pydantic Schemas
# ============================================================================

class FeatureResponse(BaseModel):
    """Schema for feature response."""
    id: int
    project_id: int
    priority: int
    category: str
    name: str
    description: Optional[str]
    steps: List[str]
    passes: bool
    in_progress: bool
    stalled: bool
    stall_reason: Optional[str]
    assigned_agent: Optional[str]
    depends_on: List[int] = []
    files_modified: List[str] = []
    created_at: datetime
    updated_at: datetime

    class Config:
        from_attributes = True

class FeatureListResponse(BaseModel):
    """Schema for feature list response."""
    features: List[FeatureResponse]
    count: int
    total: int

class FeatureCreate(BaseModel):
    """Schema for creating a feature."""
    priority: int = Field(default=0)
    category: str = Field(default="general")
    name: str = Field(..., min_length=1, max_length=255)
    description: Optional[str] = None
    steps: List[str] = Field(default_factory=list)
    depends_on: List[int] = Field(default_factory=list)
    files_modified: List[str] = Field(default_factory=list)

class FeatureUpdate(BaseModel):
    """Schema for updating a feature.

    Note: Only specific fields are allowed to be updated.
    Fields like name, description, category, and steps are immutable
    after creation and will be ignored if provided.
    """
    priority: Optional[int] = None
    passes: Optional[bool] = None
    in_progress: Optional[bool] = None
    stalled: Optional[bool] = None
    stall_reason: Optional[str] = None
    assigned_agent: Optional[str] = None
    depends_on: Optional[List[int]] = None
    files_modified: Optional[List[str]] = None

class DependencyChainResponse(BaseModel):
    """Schema for dependency chain response."""
    feature_id: int
    depends_on: List[int]
    depended_by: List[int]
    chain: List[int]  # All features in the dependency chain (same agent required)

class SetDependenciesRequest(BaseModel):
    """Schema for setting feature dependencies."""
    depends_on: List[int] = Field(default_factory=list)

class SetFilesModifiedRequest(BaseModel):
    """Schema for setting files modified by a feature."""
    files_modified: List[str] = Field(default_factory=list)

class FileConflictResponse(BaseModel):
    """Schema for file conflict detection response."""
    feature_id: int
    files_modified: List[str]
    conflicting_features: List[Dict]
    conflict_groups: List[List[int]]

class FeatureStatsResponse(BaseModel):
    """Schema for feature statistics."""
    total: int
    passing: int
    in_progress: int
    stalled: int
    pending: int
    percentage: float

# ============================================================================
# API Endpoints
# ============================================================================

@router.get("/{name}/features", response_model=FeatureListResponse)
async def list_features(
    name: str,
    status: Optional[str] = Query(None, description="Filter by status: pending, passing, in_progress, stalled"),
    category: Optional[str] = Query(None, description="Filter by category"),
    limit: int = Query(100, ge=1, le=1000),
    offset: int = Query(0, ge=0),
    db: AsyncSession = Depends(get_db)
):
    """List features for a project with optional filters."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Build query
    query = select(Feature).where(Feature.project_id == project.id)

    # Apply status filter
    if status:
        if status == "pending":
            query = query.where(and_(
                Feature.passes == False,
                Feature.in_progress == False,
                Feature.stalled == False
            ))
        elif status == "passing":
            query = query.where(Feature.passes == True)
        elif status == "in_progress":
            query = query.where(Feature.in_progress == True)
        elif status == "stalled":
            query = query.where(Feature.stalled == True)

    # Apply category filter
    if category:
        query = query.where(Feature.category == category)

    # Get total count before pagination
    count_query = select(Feature).where(Feature.project_id == project.id)
    count_result = await db.execute(count_query)
    total = len(count_result.scalars().all())

    # Apply pagination
    query = query.order_by(Feature.priority).offset(offset).limit(limit)
    result = await db.execute(query)
    features = result.scalars().all()

    return FeatureListResponse(
        features=[FeatureResponse.model_validate(f) for f in features],
        count=len(features),
        total=total
    )

@router.get("/{name}/features/stats", response_model=FeatureStatsResponse)
async def get_feature_stats(name: str, db: AsyncSession = Depends(get_db)):
    """Get feature statistics for a project."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = result.scalars().all()

    total = len(features)
    passing = sum(1 for f in features if f.passes)
    in_progress = sum(1 for f in features if f.in_progress)
    stalled = sum(1 for f in features if f.stalled)
    pending = total - passing - in_progress - stalled

    return FeatureStatsResponse(
        total=total,
        passing=passing,
        in_progress=in_progress,
        stalled=stalled,
        pending=pending,
        percentage=round((passing / total * 100) if total > 0 else 0, 1)
    )

@router.get("/{name}/features/{feature_id}", response_model=FeatureResponse)
async def get_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get a specific feature by ID."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    return FeatureResponse.model_validate(feature)

@router.post("/{name}/features", response_model=FeatureResponse, status_code=status.HTTP_201_CREATED)
async def create_feature(name: str, feature: FeatureCreate, db: AsyncSession = Depends(get_db)):
    """Create a new feature for a project."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Create feature
    db_feature = Feature(
        project_id=project.id,
        priority=feature.priority,
        category=feature.category,
        name=feature.name,
        description=feature.description,
        steps=feature.steps,
        depends_on=feature.depends_on,
        files_modified=feature.files_modified,
    )
    db.add(db_feature)
    await db.commit()
    await db.refresh(db_feature)

    return FeatureResponse.model_validate(db_feature)

@router.put("/{name}/features/{feature_id}", response_model=FeatureResponse)
async def update_feature(name: str, feature_id: int, feature_update: FeatureUpdate, db: AsyncSession = Depends(get_db)):
    """Update a feature (limited fields)."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Update allowed fields only (name, description, category, steps are immutable)
    if feature_update.priority is not None:
        feature.priority = feature_update.priority
    if feature_update.passes is not None:
        feature.passes = feature_update.passes
    if feature_update.in_progress is not None:
        feature.in_progress = feature_update.in_progress
    if feature_update.stalled is not None:
        feature.stalled = feature_update.stalled
    if feature_update.stall_reason is not None:
        feature.stall_reason = feature_update.stall_reason
    if feature_update.assigned_agent is not None:
        feature.assigned_agent = feature_update.assigned_agent
    if feature_update.depends_on is not None:
        feature.depends_on = feature_update.depends_on
    if feature_update.files_modified is not None:
        feature.files_modified = feature_update.files_modified

    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)

    return FeatureResponse.model_validate(feature)

@router.post("/{name}/features/{feature_id}/unstall", response_model=FeatureResponse)
async def unstall_feature(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Clear stalled status from a feature."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get feature
    result = await db.execute(
        select(Feature).where(and_(
            Feature.id == feature_id,
            Feature.project_id == project.id
        ))
    )
    feature = result.scalar_one_or_none()

    if not feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    feature.stalled = False
    feature.stall_reason = None
    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)

    return FeatureResponse.model_validate(feature)

# ============================================================================
# Dependency Management Endpoints
# ============================================================================

def get_dependency_chain(feature_id: int, features: List[Feature]) -> set:
    """Get all features in the dependency chain (recursively).

    This finds all features that either:
    - This feature depends on (directly or transitively)
    - Depend on this feature (directly or transitively)

    All features in this chain must be assigned to the same agent.
    """
    feature_map = {f.id: f for f in features}
    chain = set()
    visited = set()

    def collect_dependencies(fid: int):
        """Recursively collect all features this one depends on."""
        if fid in visited or fid not in feature_map:
            return
        visited.add(fid)
        chain.add(fid)
        feature = feature_map[fid]
        for dep_id in (feature.depends_on or []):
            collect_dependencies(dep_id)

    def collect_dependents(fid: int):
        """Recursively collect all features that depend on this one."""
        if fid in visited:
            return
        visited.add(fid)
        chain.add(fid)
        for f in features:
            if fid in (f.depends_on or []):
                collect_dependents(f.id)

    # Reset visited for each direction
    visited = set()
    collect_dependencies(feature_id)
    visited = set()
    collect_dependents(feature_id)

    return chain

def get_file_conflict_groups(features: List[Feature]) -> Dict[str, Set[int]]:
    """Build a mapping of files to feature IDs that modify them."""
    file_to_features: Dict[str, Set[int]] = {}
    for feature in features:
        for file_path in (feature.files_modified or []):
            if file_path not in file_to_features:
                file_to_features[file_path] = set()
            file_to_features[file_path].add(feature.id)
    return file_to_features

def get_features_sharing_files(feature_id: int, features: List[Feature]) -> Set[int]:
    """Get all feature IDs that share files with the given feature."""
    feature_map = {f.id: f for f in features}
    target_feature = feature_map.get(feature_id)
    if not target_feature or not target_feature.files_modified:
        return {feature_id}
    file_to_features = get_file_conflict_groups(features)
    related_features = set()
    for file_path in target_feature.files_modified:
        if file_path in file_to_features:
            related_features.update(file_to_features[file_path])
    return related_features

def get_full_conflict_chain(feature_id: int, features: List[Feature]) -> Set[int]:
    """Get all features that must be on the same agent due to dependencies OR file conflicts."""
    dep_chain = get_dependency_chain(feature_id, features)
    all_related = set(dep_chain)
    changed = True
    while changed:
        changed = False
        new_related = set()
        for fid in all_related:
            file_conflicts = get_features_sharing_files(fid, features)
            for conflict_fid in file_conflicts:
                if conflict_fid not in all_related:
                    new_related.add(conflict_fid)
                    new_related.update(get_dependency_chain(conflict_fid, features))
        if new_related:
            all_related.update(new_related)
            changed = True
    return all_related

@router.get("/{name}/features/{feature_id}/dependencies", response_model=DependencyChainResponse)
async def get_feature_dependencies(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get dependency information for a feature."""
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = result.scalars().all()

    # Find the target feature
    target_feature = None
    for f in features:
        if f.id == feature_id:
            target_feature = f
            break

    if not target_feature:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Get features that depend on this one
    depended_by = [f.id for f in features if feature_id in (f.depends_on or [])]

    # Get full dependency chain
    chain = get_dependency_chain(feature_id, features)

    return DependencyChainResponse(
        feature_id=feature_id,
        depends_on=target_feature.depends_on or [],
        depended_by=depended_by,
        chain=sorted(list(chain))
    )

@router.put("/{name}/features/{feature_id}/dependencies", response_model=FeatureResponse)
async def set_feature_dependencies(
    name: str,
    feature_id: int,
    request: SetDependenciesRequest,
    db: AsyncSession = Depends(get_db)
):
    """Set dependencies for a feature.

    If the feature or any of its dependencies are already assigned to an agent,
    all features in the dependency chain will be assigned to the same agent.
    """
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}

    # Find the target feature
    if feature_id not in feature_map:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    target_feature = feature_map[feature_id]

    # Validate all dependency IDs exist in the same project
    for dep_id in request.depends_on:
        if dep_id not in feature_map:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Dependency feature {dep_id} not found in project '{name}'"
            )
        if dep_id == feature_id:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="A feature cannot depend on itself"
            )

    # Detect circular dependencies - check if any new dependency would create a path back to target
    def has_circular_dependency(target_id: int, new_deps: List[int]) -> bool:
        """Check if adding new_deps to target_id creates a cycle.

        A cycle exists if any of the new dependencies eventually depend on target_id.
        For example: if we add dep 19 to feature 20, check if 19 depends on 20 (directly or indirectly).
        """
        def feature_depends_on(fid: int, check_id: int, visited: set) -> bool:
            """Check if feature fid depends (directly or indirectly) on check_id."""
            if fid == check_id:
                return True
            if fid in visited:
                return False
            visited.add(fid)

            feature = feature_map.get(fid)
            if not feature:
                return False

            deps = feature.depends_on or []
            for dep_id in deps:
                if feature_depends_on(dep_id, check_id, visited):
                    return True
            return False

        # Check: would any new dependency create a path back to target_id?
        for dep_id in new_deps:
            if feature_depends_on(dep_id, target_id, set()):
                return True
        return False

    if has_circular_dependency(feature_id, request.depends_on):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Circular dependency detected"
        )

    # Update the dependencies
    target_feature.depends_on = request.depends_on
    target_feature.updated_at = datetime.utcnow()

    # Update the feature map for chain calculation
    features = [f if f.id != feature_id else target_feature for f in features]

    # Get the full dependency chain
    chain = get_dependency_chain(feature_id, features)

    # Check if any feature in the chain has an assigned agent
    assigned_agent = None
    for fid in chain:
        if feature_map[fid].assigned_agent:
            if assigned_agent is None:
                assigned_agent = feature_map[fid].assigned_agent
            elif assigned_agent != feature_map[fid].assigned_agent:
                # Conflict: different agents assigned to features in the same chain
                # Prioritize keeping all in the same chain
                pass

    # If any feature in the chain has an agent, assign all to that agent
    if assigned_agent:
        for fid in chain:
            if feature_map[fid].assigned_agent != assigned_agent:
                feature_map[fid].assigned_agent = assigned_agent
                feature_map[fid].updated_at = datetime.utcnow()

    await db.commit()
    await db.refresh(target_feature)

    return FeatureResponse.model_validate(target_feature)

@router.put("/{name}/features/{feature_id}/files-modified", response_model=FeatureResponse)
async def set_files_modified(
    name: str,
    feature_id: int,
    request: SetFilesModifiedRequest,
    db: AsyncSession = Depends(get_db)
):
    """Set the list of files modified by a feature."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(and_(Feature.id == feature_id, Feature.project_id == project.id)))
    feature = result.scalar_one_or_none()
    if not feature:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
    feature.files_modified = request.files_modified
    feature.updated_at = datetime.utcnow()
    await db.commit()
    await db.refresh(feature)
    return FeatureResponse.model_validate(feature)

@router.get("/{name}/features/{feature_id}/file-conflicts", response_model=FileConflictResponse)
async def get_file_conflicts(name: str, feature_id: int, db: AsyncSession = Depends(get_db)):
    """Get file conflict information for a feature."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}
    if feature_id not in feature_map:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature {feature_id} not found")
    target_feature = feature_map[feature_id]
    file_to_features = get_file_conflict_groups(features)
    conflicting_features = []
    seen_ids = {feature_id}
    for file_path in (target_feature.files_modified or []):
        if file_path in file_to_features:
            for fid in file_to_features[file_path]:
                if fid not in seen_ids:
                    seen_ids.add(fid)
                    f = feature_map[fid]
                    shared_files = [fp for fp in (f.files_modified or []) if fp in (target_feature.files_modified or [])]
                    conflicting_features.append({"feature_id": fid, "name": f.name, "files": shared_files, "assigned_agent": f.assigned_agent})
    conflict_groups = []
    for file_path, fids in file_to_features.items():
        if len(fids) > 1 and feature_id in fids:
            conflict_groups.append(sorted(list(fids)))
    return FileConflictResponse(feature_id=feature_id, files_modified=target_feature.files_modified or [], conflicting_features=conflicting_features, conflict_groups=conflict_groups)

@router.get("/{name}/features/file-conflicts", response_model=List[Dict])
async def get_all_file_conflicts(name: str, db: AsyncSession = Depends(get_db)):
    """Get all file conflicts in the project."""
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()
    if not project:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{name}' not found")
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}
    file_to_features = get_file_conflict_groups(features)
    conflicts = []
    for file_path, fids in file_to_features.items():
        if len(fids) > 1:
            feature_info = []
            agents = set()
            for fid in fids:
                f = feature_map[fid]
                feature_info.append({"feature_id": fid, "name": f.name, "assigned_agent": f.assigned_agent})
                if f.assigned_agent:
                    agents.add(f.assigned_agent)
            conflicts.append({"file_path": file_path, "feature_count": len(fids), "features": feature_info, "has_agent_conflict": len(agents) > 1})
    return conflicts

@router.post("/{name}/features/{feature_id}/assign-agent", response_model=List[FeatureResponse])
async def assign_feature_to_agent(
    name: str,
    feature_id: int,
    agent_id: str = Query(..., regex="^[AB]$", description="Agent ID (A or B)"),
    db: AsyncSession = Depends(get_db)
):
    """Assign a feature and all its dependencies/file conflicts to an agent.

    This ensures that all features in the dependency chain AND file conflict
    chain are assigned to the same agent to prevent conflicts.
    """
    # Verify project exists
    result = await db.execute(select(Project).where(Project.name == name))
    project = result.scalar_one_or_none()

    if not project:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Project '{name}' not found"
        )

    # Get all features for the project
    result = await db.execute(select(Feature).where(Feature.project_id == project.id))
    features = list(result.scalars().all())
    feature_map = {f.id: f for f in features}

    # Find the target feature
    if feature_id not in feature_map:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Feature {feature_id} not found in project '{name}'"
        )

    # Get the full conflict chain (dependencies AND file conflicts)
    chain = get_full_conflict_chain(feature_id, features)

    # Check for conflicts with other agent
    other_agent = "B" if agent_id == "A" else "A"
    conflicting_features = [
        fid for fid in chain
        if feature_map[fid].assigned_agent == other_agent
    ]

    if conflicting_features:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Features {conflicting_features} in dependency/file conflict chain are assigned to Agent {other_agent}"
        )

    # Assign all features in the chain to the agent
    updated_features = []
    for fid in chain:
        feature = feature_map[fid]
        if feature.assigned_agent != agent_id:
            feature.assigned_agent = agent_id
            feature.updated_at = datetime.utcnow()
        updated_features.append(feature)

    await db.commit()

    # Refresh all updated features
    for feature in updated_features:
        await db.refresh(feature)

    return [FeatureResponse.model_validate(f) for f in updated_features]


All changes have been successfully applied to the file. Here is a summary of the updates made to `C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\backend\app\routers\features.py`:

## Summary of Changes

### 1. Updated Imports (Line 9)
Changed `from typing import Optional, List` to `from typing import Optional, List, Dict, Set`

### 2. Updated FeatureResponse Schema (Line 41)
Added `files_modified: List[str] = []` after `depends_on`

### 3. Updated FeatureCreate Schema (Line 64)
Added `files_modified: List[str] = Field(default_factory=list)`

### 4. Updated FeatureUpdate Schema (Line 81)
Added `files_modified: Optional[List[str]] = None`

### 5. Added New Schemas (Lines 97-107)
- `SetFilesModifiedRequest` - Schema for setting files modified by a feature
- `FileConflictResponse` - Schema for file conflict detection response

### 6. Added Helper Functions (Lines 414-456)
- `get_file_conflict_groups()` - Builds a mapping of files to feature IDs that modify them
- `get_features_sharing_files()` - Gets all feature IDs that share files with a given feature
- `get_full_conflict_chain()` - Gets all features that must be on the same agent due to dependencies OR file conflicts

### 7. Updated update_feature Function (Lines 320-322)
Added handling for `files_modified` field updates

### 8. Updated create_feature Function (Line 269)
Added `files_modified=feature.files_modified` to the Feature creation

### 9. Added New Endpoints (Lines 623-699)
- `PUT /{name}/features/{feature_id}/files-modified` - Set files modified by a feature
- `GET /{name}/features/{feature_id}/file-conflicts` - Get file conflict information for a specific feature
- `GET /{name}/features/file-conflicts` - Get all file conflicts in the project

### 10. Updated assign_feature_to_agent Function (Lines 702-767)
- Updated docstring to mention file conflicts
- Changed from `get_dependency_chain()` to `get_full_conflict_chain()` (Line 737)
- Updated error message to include "dependency/file conflict chain" (Line 749)