def list_valid_projects() -> list[dict[str, Any]]:
    """
    List all projects that have valid, accessible paths.

    Returns:
        List of project info dicts with additional 'name' field.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        projects = session.query(Project).all()
        valid = []
        for p in projects:
            path = Path(p.path)
            is_valid, _ = validate_project_path(path)
            if is_valid:
                valid.append({
                    "name": p.name,
                    "path": p.path,
                    "created_at": p.created_at.isoformat() if p.created_at else None
                })
        return valid
    finally:
        session.close()

# =============================================================================
# Reference Folder Functions
# =============================================================================

def add_reference_folder(
    project_name: str,
    path: Path,
    alias: str | None = None,
    description: str | None = None,
) -> int:
    """
    Add a reference folder to a project.

    Args:
        project_name: The project to add the reference to.
        path: The absolute path to the reference folder.
        alias: Optional friendly name for the reference.
        description: Optional description of what this reference is for.

    Returns:
        The ID of the created reference folder.

    Raises:
        RegistryError: If the project doesn't exist.
        ValueError: If the path is not valid.
    """
    path = Path(path).resolve()

    # Validate path exists and is a directory
    if not path.exists():
        raise ValueError(f"Reference path does not exist: {path}")
    if not path.is_dir():
        raise ValueError(f"Reference path is not a directory: {path}")

    with _get_session() as session:
        # Check project exists
        project = session.query(Project).filter(Project.name == project_name).first()
        if not project:
            raise RegistryError(f"Project '{project_name}' not found")

        # Check for duplicate path
        existing = session.query(ReferenceFolder).filter(
            ReferenceFolder.project_name == project_name,
            ReferenceFolder.path == path.as_posix()
        ).first()
        if existing:
            raise RegistryError(f"Reference folder already exists for this project")

        ref = ReferenceFolder(
            project_name=project_name,
            path=path.as_posix(),
            alias=alias,
            description=description,
            created_at=datetime.now(),
        )
        session.add(ref)
        session.flush()  # Get the ID
        ref_id = ref.id

    logger.info("Added reference folder to '%s': %s", project_name, path)
    return ref_id

def remove_reference_folder(reference_id: int) -> bool:
    """
    Remove a reference folder by ID.

    Args:
        reference_id: The ID of the reference folder to remove.

    Returns:
        True if removed, False if not found.
    """
    with _get_session() as session:
        ref = session.query(ReferenceFolder).filter(ReferenceFolder.id == reference_id).first()
        if not ref:
            return False
        session.delete(ref)

    logger.info("Removed reference folder ID %d", reference_id)
    return True

def list_reference_folders(project_name: str) -> list[dict[str, Any]]:
    """
    List all reference folders for a project.

    Args:
        project_name: The project name.

    Returns:
        List of reference folder info dicts.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        refs = session.query(ReferenceFolder).filter(
            ReferenceFolder.project_name == project_name
        ).all()
        return [
            {
                "id": r.id,
                "path": r.path,
                "alias": r.alias,
                "description": r.description,
                "created_at": r.created_at.isoformat() if r.created_at else None,
                "exists": Path(r.path).exists(),
            }
            for r in refs
        ]
    finally:
        session.close()

def get_reference_folder(reference_id: int) -> dict[str, Any] | None:
    """
    Get a reference folder by ID.

    Args:
        reference_id: The reference folder ID.

    Returns:
        Reference folder info dict, or None if not found.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        ref = session.query(ReferenceFolder).filter(ReferenceFolder.id == reference_id).first()
        if not ref:
            return None
        return {
            "id": ref.id,
            "project_name": ref.project_name,
            "path": ref.path,
            "alias": ref.alias,
            "description": ref.description,
            "created_at": ref.created_at.isoformat() if ref.created_at else None,
            "exists": Path(ref.path).exists(),
        }
    finally:
        session.close()

def update_reference_folder(
    reference_id: int,
    alias: str | None = None,
    description: str | None = None,
) -> bool:
    """
    Update a reference folder's metadata.

    Args:
        reference_id: The reference folder ID.
        alias: New alias (pass None to keep unchanged).
        description: New description (pass None to keep unchanged).

    Returns:
        True if updated, False if not found.
    """
    with _get_session() as session:
        ref = session.query(ReferenceFolder).filter(ReferenceFolder.id == reference_id).first()
        if not ref:
            return False

        if alias is not None:
            ref.alias = alias
        if description is not None:
            ref.description = description

    return True

"""
Project Registry Module
=======================

Cross-platform project registry for storing project name to path mappings.
Uses SQLite database stored at ~/.autocoder/registry.db.
"""

import logging
import os
import re
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any

from sqlalchemy import Column, DateTime, Integer, String, Text, ForeignKey, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Module logger
logger = logging.getLogger(__name__)

# =============================================================================
# Exceptions
# =============================================================================

class RegistryError(Exception):
    """Base registry exception."""
    pass

class RegistryNotFound(RegistryError):
    """Registry file doesn't exist."""
    pass

class RegistryCorrupted(RegistryError):
    """Registry database is corrupted."""
    pass

class RegistryPermissionDenied(RegistryError):
    """Can't read/write registry file."""
    pass

# =============================================================================
# SQLAlchemy Model
# =============================================================================

Base = declarative_base()

class Project(Base):
    """SQLAlchemy model for registered projects."""
    __tablename__ = "projects"

    name = Column(String(50), primary_key=True, index=True)
    path = Column(String, nullable=False)  # POSIX format for cross-platform
    created_at = Column(DateTime, nullable=False)

class ReferenceFolder(Base):
    """SQLAlchemy model for reference folders linked to projects."""
    __tablename__ = "reference_folders"

    id = Column(Integer, primary_key=True, autoincrement=True)
    project_name = Column(String(50), ForeignKey("projects.name", ondelete="CASCADE"), nullable=False, index=True)
    path = Column(String, nullable=False)  # POSIX format
    alias = Column(String(100), nullable=True)  # Optional friendly name
    description = Column(Text, nullable=True)  # What this reference is for
    created_at = Column(DateTime, nullable=False)

# =============================================================================
# Database Connection
# =============================================================================

# Module-level singleton for database engine
_engine = None
_SessionLocal = None

def get_config_dir() -> Path:
    """
    Get the config directory: ~/.autocoder/

    Returns:
        Path to ~/.autocoder/ (created if it doesn't exist)
    """
    config_dir = Path.home() / ".autocoder"
    config_dir.mkdir(parents=True, exist_ok=True)
    return config_dir

def get_registry_path() -> Path:
    """Get the path to the registry database."""
    return get_config_dir() / "registry.db"

def _get_engine():
    """
    Get or create the database engine (singleton pattern).

    Returns:
        Tuple of (engine, SessionLocal)
    """
    global _engine, _SessionLocal

    if _engine is None:
        db_path = get_registry_path()
        db_url = f"sqlite:///{db_path.as_posix()}"
        _engine = create_engine(db_url, connect_args={"check_same_thread": False})
        Base.metadata.create_all(bind=_engine)
        _SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
        logger.debug("Initialized registry database at: %s", db_path)

    return _engine, _SessionLocal

@contextmanager
def _get_session():
    """
    Context manager for database sessions with automatic commit/rollback.

    Yields:
        SQLAlchemy session
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

# =============================================================================
# Project CRUD Functions
# =============================================================================

def register_project(name: str, path: Path) -> None:
    """
    Register a new project in the registry.

    Args:
        name: The project name (unique identifier).
        path: The absolute path to the project directory.

    Raises:
        ValueError: If project name is invalid or path is not absolute.
        RegistryError: If a project with that name already exists.
    """
    # Validate name
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
        raise ValueError(
            "Invalid project name. Use only letters, numbers, hyphens, "
            "and underscores (1-50 chars)."
        )

    # Ensure path is absolute
    path = Path(path).resolve()

    with _get_session() as session:
        existing = session.query(Project).filter(Project.name == name).first()
        if existing:
            logger.warning("Attempted to register duplicate project: %s", name)
            raise RegistryError(f"Project '{name}' already exists in registry")

        project = Project(
            name=name,
            path=path.as_posix(),
            created_at=datetime.now()
        )
        session.add(project)

    logger.info("Registered project '%s' at path: %s", name, path)

def unregister_project(name: str) -> bool:
    """
    Remove a project from the registry.

    Args:
        name: The project name to remove.

    Returns:
        True if removed, False if project wasn't found.
    """
    with _get_session() as session:
        project = session.query(Project).filter(Project.name == name).first()
        if not project:
            logger.debug("Attempted to unregister non-existent project: %s", name)
            return False

        session.delete(project)

    logger.info("Unregistered project: %s", name)
    return True

def get_project_path(name: str) -> Path | None:
    """
    Look up a project's path by name.

    Args:
        name: The project name.

    Returns:
        The project Path, or None if not found.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        project = session.query(Project).filter(Project.name == name).first()
        if project is None:
            return None
        return Path(project.path)
    finally:
        session.close()

def list_registered_projects() -> dict[str, dict[str, Any]]:
    """
    Get all registered projects.

    Returns:
        Dictionary mapping project names to their info dictionaries.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        projects = session.query(Project).all()
        return {
            p.name: {
                "path": p.path,
                "created_at": p.created_at.isoformat() if p.created_at else None
            }
            for p in projects
        }
    finally:
        session.close()

def get_project_info(name: str) -> dict[str, Any] | None:
    """
    Get full info about a project.

    Args:
        name: The project name.

    Returns:
        Project info dictionary, or None if not found.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        project = session.query(Project).filter(Project.name == name).first()
        if project is None:
            return None
        return {
            "path": project.path,
            "created_at": project.created_at.isoformat() if project.created_at else None
        }
    finally:
        session.close()

def update_project_path(name: str, new_path: Path) -> bool:
    """
    Update a project's path (for relocating projects).

    Args:
        name: The project name.
        new_path: The new absolute path.

    Returns:
        True if updated, False if project wasn't found.
    """
    new_path = Path(new_path).resolve()

    with _get_session() as session:
        project = session.query(Project).filter(Project.name == name).first()
        if not project:
            return False

        project.path = new_path.as_posix()

    return True

# =============================================================================
# Validation Functions
# =============================================================================

def validate_project_path(path: Path) -> tuple[bool, str]:
    """
    Validate that a project path is accessible and writable.

    Args:
        path: The path to validate.

    Returns:
        Tuple of (is_valid, error_message).
    """
    path = Path(path).resolve()

    # Check if path exists
    if not path.exists():
        return False, f"Path does not exist: {path}"

    # Check if it's a directory
    if not path.is_dir():
        return False, f"Path is not a directory: {path}"

    # Check read permissions
    if not os.access(path, os.R_OK):
        return False, f"No read permission: {path}"

    # Check write permissions
    if not os.access(path, os.W_OK):
        return False, f"No write permission: {path}"

    return True, ""

def cleanup_stale_projects() -> list[str]:
    """
    Remove projects from registry whose paths no longer exist.

    Returns:
        List of removed project names.
    """
    removed = []

    with _get_session() as session:
        projects = session.query(Project).all()
        for project in projects:
            path = Path(project.path)
            if not path.exists():
                session.delete(project)
                removed.append(project.name)

    if removed:
        logger.info("Cleaned up stale projects: %s", removed)

    return removed

def list_valid_projects() -> list[dict[str, Any]]:
    """
    List all projects that have valid, accessible paths.

    Returns:
        List of project info dicts with additional 'name' field.
    """
    _, SessionLocal = _get_engine()
    session = SessionLocal()
    try:
        projects = session.query(Project).all()
        valid = []
        for p in projects:
            path = Path(p.path)
            is_valid, _ = validate_project_path(path)
            if is_valid:
                valid.append({
                    "name": p.name,
                    "path": p.path,
                    "created_at": p.created_at.isoformat() if p.created_at else None
                })
        return valid
    finally:
        session.close()

+    Add a reference folder to a project.

+        project_name: The project to add the reference to.

+        path: The absolute path to the reference folder.

+        alias: Optional friendly name for the reference.

+        description: Optional description of what this reference is for.

+        The ID of the created reference folder.

+        RegistryError: If the project doesn't exist.

+        ValueError: If the path is not valid.

+    # Validate path exists and is a directory

+        raise ValueError(f"Reference path does not exist: {path}")

+        raise ValueError(f"Reference path is not a directory: {path}")

+        project = session.query(Project).filter(Project.name == project_name).first()

+            raise RegistryError(f"Project '{project_name}' not found")

+        existing = session.query(ReferenceFolder).filter(

+            ReferenceFolder.project_name == project_name,

+            ReferenceFolder.path == path.as_posix()

+            raise RegistryError(f"Reference folder already exists for this project")

+    logger.info("Added reference folder to '%s': %s", project_name, path)

+def remove_reference_folder(reference_id: int) -> bool:

+        reference_id: The ID of the reference folder to remove.

+        True if removed, False if not found.

+        ref = session.query(ReferenceFolder).filter(ReferenceFolder.id == reference_id).first()

+    logger.info("Removed reference folder ID %d", reference_id)

+def list_reference_folders(project_name: str) -> list[dict[str, Any]]:

+    List all reference folders for a project.

+        project_name: The project name.

+        List of reference folder info dicts.

+        refs = session.query(ReferenceFolder).filter(

+                "description": r.description,

+                "created_at": r.created_at.isoformat() if r.created_at else None,

+                "exists": Path(r.path).exists(),

+def get_reference_folder(reference_id: int) -> dict[str, Any] | None:

+        reference_id: The reference folder ID.

+        Reference folder info dict, or None if not found.

+            "project_name": ref.project_name,

+            "description": ref.description,

+            "created_at": ref.created_at.isoformat() if ref.created_at else None,

+            "exists": Path(ref.path).exists(),

+    Update a reference folder's metadata.

+        alias: New alias (pass None to keep unchanged).

+        description: New description (pass None to keep unchanged).

+        True if updated, False if not found.

+            ref.description = description


Now I need to update the TodoWrite and create the API endpoints.


TodoWrite

Add reference_folders table to registry database

Adding reference_folders table to registry

Create API endpoints for reference folder management

Creating API endpoints for reference folders

Inject reference folders into agent prompts

Injecting reference folders into agent prompts

Create UI for managing reference folders

Creating UI for reference folder management

Build and test


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Add reference_folders table to registry database

Adding reference_folders table to registry

Create API endpoints for reference folder management

Creating API endpoints for reference folders

Inject reference folders into agent prompts

Injecting reference folders into agent prompts

Create UI for managing reference folders

Creating UI for reference folder management

Build and test

Build and test


Now I need to create the API router for reference folders. I'll add it to the projects router or create a new router. Since it's related to projects, I'll add endpoints to the projects router.


    55→
    56→
    57→def _get_registry_functions():
    58→    """Get registry functions with lazy import."""
    59→    import sys
    60→    root = Path(__file__).parent.parent.parent
    61→    if str(root) not in sys.path:
    62→        sys.path.insert(0, str(root))
    63→
    64→    from registry import (
    65→        get_project_path,
    66→        list_registered_projects,
    67→        register_project,
    68→        unregister_project,
    69→        validate_project_path,
    70→    )
    71→    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path
    72→
    73→
    74→router = APIRouter(prefix="/api/projects", tags=["projects"])
    75→
    76→
    77→def validate_project_name(name: str) -> str:
    78→    """Validate and sanitize project name to prevent path traversal."""
    79→    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):

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

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

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

def validate_project_name(name: str) -> str:
    """Validate and sanitize project name to prevent path traversal."""
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):


I see the import pattern. Let me update the import function and add the reference folder endpoints. First, let me update the _get_registry_functions to include reference folder functions, then add the endpoints.


Edit

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

def _get_reference_functions():
    """Get reference folder functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        add_reference_folder,
        remove_reference_folder,
        list_reference_folders,
        get_reference_folder,
        update_reference_folder,
    )
    return add_reference_folder, remove_reference_folder, list_reference_folders, get_reference_folder, update_reference_folder


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\routers\projects.py has been updated successfully.

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

def _get_reference_functions():
    """Get reference folder functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        add_reference_folder,
        remove_reference_folder,
        list_reference_folders,
        get_reference_folder,
        update_reference_folder,
    )
    return add_reference_folder, remove_reference_folder, list_reference_folders, get_reference_folder, update_reference_folder

"""
Projects Router
===============

API endpoints for project management.
Uses project registry for path lookups instead of fixed generations/ directory.
"""

import re
import shutil
from pathlib import Path

from fastapi import APIRouter, HTTPException

from ..schemas import (
    ProjectCreate,
    ProjectDetail,
    ProjectPrompts,
    ProjectPromptsUpdate,
    ProjectStats,
    ProjectSummary,
)

# Lazy imports to avoid circular dependencies
_imports_initialized = False
_check_spec_exists = None
_scaffold_project_prompts = None
_get_project_prompts_dir = None
_count_passing_tests = None

def _init_imports():
    """Lazy import of project-level modules."""
    global _imports_initialized, _check_spec_exists
    global _scaffold_project_prompts, _get_project_prompts_dir
    global _count_passing_tests

    if _imports_initialized:
        return

    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from progress import count_passing_tests
    from prompts import get_project_prompts_dir, scaffold_project_prompts
    from start import check_spec_exists

    _check_spec_exists = check_spec_exists
    _scaffold_project_prompts = scaffold_project_prompts
    _get_project_prompts_dir = get_project_prompts_dir
    _count_passing_tests = count_passing_tests
    _imports_initialized = True

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

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

def validate_project_name(name: str) -> str:
    """Validate and sanitize project name to prevent path traversal."""
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
        raise HTTPException(
            status_code=400,
            detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
        )
    return name

def get_project_stats(project_dir: Path) -> ProjectStats:
    """Get statistics for a project."""
    _init_imports()
    passing, in_progress, total = _count_passing_tests(project_dir)
    percentage = (passing / total * 100) if total > 0 else 0.0
    return ProjectStats(
        passing=passing,
        in_progress=in_progress,
        total=total,
        percentage=round(percentage, 1)
    )

@router.get("", response_model=list[ProjectSummary])
async def list_projects():
    """List all registered projects."""
    _init_imports()
    _, _, _, list_registered_projects, validate_project_path = _get_registry_functions()

    projects = list_registered_projects()
    result = []

    for name, info in projects.items():
        project_dir = Path(info["path"])

        # Skip if path no longer exists
        is_valid, _ = validate_project_path(project_dir)
        if not is_valid:
            continue

        has_spec = _check_spec_exists(project_dir)
        stats = get_project_stats(project_dir)

        result.append(ProjectSummary(
            name=name,
            path=info["path"],
            has_spec=has_spec,
            stats=stats,
        ))

    return result

@router.post("", response_model=ProjectSummary)
async def create_project(project: ProjectCreate):
    """Create a new project at the specified path."""
    _init_imports()
    register_project, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(project.name)
    project_path = Path(project.path).resolve()

    # Check if project name already registered
    existing = get_project_path(name)
    if existing:
        raise HTTPException(
            status_code=409,
            detail=f"Project '{name}' already exists at {existing}"
        )

    # Security: Check if path is in a blocked location
    from .filesystem import is_path_blocked
    if is_path_blocked(project_path):
        raise HTTPException(
            status_code=403,
            detail="Cannot create project in system or sensitive directory"
        )

    # Validate the path is usable
    if project_path.exists():
        if not project_path.is_dir():
            raise HTTPException(
                status_code=400,
                detail="Path exists but is not a directory"
            )
    else:
        # Create the directory
        try:
            project_path.mkdir(parents=True, exist_ok=True)
        except OSError as e:
            raise HTTPException(
                status_code=500,
                detail=f"Failed to create directory: {e}"
            )

    # Scaffold prompts
    _scaffold_project_prompts(project_path)

    # Register in registry
    try:
        register_project(name, project_path)
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to register project: {e}"
        )

    return ProjectSummary(
        name=name,
        path=project_path.as_posix(),
        has_spec=False,  # Just created, no spec yet
        stats=ProjectStats(passing=0, total=0, percentage=0.0),
    )

@router.get("/{name}", response_model=ProjectDetail)
async def get_project(name: str):
    """Get detailed information about a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found in registry")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail=f"Project directory no longer exists: {project_dir}")

    has_spec = _check_spec_exists(project_dir)
    stats = get_project_stats(project_dir)
    prompts_dir = _get_project_prompts_dir(project_dir)

    return ProjectDetail(
        name=name,
        path=project_dir.as_posix(),
        has_spec=has_spec,
        stats=stats,
        prompts_dir=str(prompts_dir),
    )

@router.delete("/{name}")
async def delete_project(name: str, delete_files: bool = False):
    """
    Delete a project from the registry.

    Args:
        name: Project name to delete
        delete_files: If True, also delete the project directory and files
    """
    _init_imports()
    _, unregister_project, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Check if agent is running
    lock_file = project_dir / ".agent.lock"
    if lock_file.exists():
        raise HTTPException(
            status_code=409,
            detail="Cannot delete project while agent is running. Stop the agent first."
        )

    # Optionally delete files
    if delete_files and project_dir.exists():
        try:
            shutil.rmtree(project_dir)
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Failed to delete project files: {e}")

    # Unregister from registry
    unregister_project(name)

    return {
        "success": True,
        "message": f"Project '{name}' deleted" + (" (files removed)" if delete_files else " (files preserved)")
    }

@router.get("/{name}/prompts", response_model=ProjectPrompts)
async def get_project_prompts(name: str):
    """Get the content of project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)

    def read_file(filename: str) -> str:
        filepath = prompts_dir / filename
        if filepath.exists():
            try:
                return filepath.read_text(encoding="utf-8")
            except Exception:
                return ""
        return ""

    return ProjectPrompts(
        app_spec=read_file("app_spec.txt"),
        initializer_prompt=read_file("initializer_prompt.md"),
        coding_prompt=read_file("coding_prompt.md"),
    )

@router.put("/{name}/prompts")
async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
    """Update project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)
    prompts_dir.mkdir(parents=True, exist_ok=True)

    def write_file(filename: str, content: str | None):
        if content is not None:
            filepath = prompts_dir / filename
            filepath.write_text(content, encoding="utf-8")

    write_file("app_spec.txt", prompts.app_spec)
    write_file("initializer_prompt.md", prompts.initializer_prompt)
    write_file("coding_prompt.md", prompts.coding_prompt)

    return {"success": True, "message": "Prompts updated"}

@router.get("/{name}/stats", response_model=ProjectStats)
async def get_project_stats_endpoint(name: str):
    """Get current progress statistics for a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)

+    """Get reference folder functions with lazy import."""

+    root = Path(__file__).parent.parent.parent

+    return add_reference_folder, remove_reference_folder, list_reference_folders, get_reference_folder, update_reference_folder


Now I need to add the Pydantic schemas and API endpoints for reference folders. Let me first check the schemas file to see what exists.


     1→"""
     2→Pydantic Schemas
     3→================
     4→
     5→Request/Response models for the API endpoints.
     6→"""
     7→
     8→import base64
     9→from datetime import datetime
    10→from typing import Literal
    11→
    12→from pydantic import BaseModel, Field, field_validator
    13→
    14→# ============================================================================
    15→# Project Schemas
    16→# ============================================================================
    17→
    18→class ProjectCreate(BaseModel):
    19→    """Request schema for creating a new project."""
    20→    name: str = Field(..., min_length=1, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
    21→    path: str = Field(..., min_length=1, description="Absolute path to project directory")
    22→    spec_method: Literal["claude", "manual"] = "claude"
    23→
    24→
    25→class ProjectStats(BaseModel):
    26→    """Project statistics."""
    27→    passing: int = 0
    28→    in_progress: int = 0
    29→    total: int = 0
    30→    percentage: float = 0.0
    31→
    32→
    33→class ProjectSummary(BaseModel):
    34→    """Summary of a project for list view."""
    35→    name: str
    36→    path: str
    37→    has_spec: bool
    38→    stats: ProjectStats
    39→
    40→
    41→class ProjectDetail(BaseModel):
    42→    """Detailed project information."""
    43→    name: str
    44→    path: str
    45→    has_spec: bool
    46→    stats: ProjectStats
    47→    prompts_dir: str
    48→
    49→
    50→class ProjectPrompts(BaseModel):
    51→    """Project prompt files content."""
    52→    app_spec: str = ""
    53→    initializer_prompt: str = ""
    54→    coding_prompt: str = ""
    55→
    56→
    57→class ProjectPromptsUpdate(BaseModel):
    58→    """Request schema for updating project prompts."""
    59→    app_spec: str | None = None
    60→    initializer_prompt: str | None = None
    61→    coding_prompt: str | None = None
    62→
    63→
    64→# ============================================================================
    65→# Feature Schemas
    66→# ============================================================================
    67→
    68→class FeatureBase(BaseModel):
    69→    """Base feature attributes."""
    70→    category: str
    71→    name: str
    72→    description: str
    73→    steps: list[str]
    74→
    75→
    76→class FeatureCreate(FeatureBase):
    77→    """Request schema for creating a new feature."""
    78→    priority: int | None = None
    79→
    80→
    81→class FeatureResponse(FeatureBase):
    82→    """Response schema for a feature."""
    83→    id: int
    84→    priority: int
    85→    passes: bool
    86→    in_progress: bool
    87→
    88→    class Config:
    89→        from_attributes = True
    90→
    91→
    92→class FeatureListResponse(BaseModel):
    93→    """Response containing list of features organized by status."""
    94→    pending: list[FeatureResponse]
    95→    in_progress: list[FeatureResponse]
    96→    done: list[FeatureResponse]
    97→
    98→
    99→# ============================================================================
   100→# Agent Schemas
   101→# ============================================================================
   102→
   103→class AgentStartRequest(BaseModel):
   104→    """Request schema for starting the agent."""
   105→    yolo_mode: bool = False
   106→
   107→
   108→class AgentStatus(BaseModel):
   109→    """Current agent status."""
   110→    status: Literal["stopped", "running", "paused", "crashed"]
   111→    pid: int | None = None
   112→    started_at: datetime | None = None
   113→    yolo_mode: bool = False
   114→
   115→
   116→class AgentActionResponse(BaseModel):
   117→    """Response for agent control actions."""
   118→    success: bool
   119→    status: str
   120→    message: str = ""
   121→
   122→
   123→# ============================================================================
   124→# Setup Schemas
   125→# ============================================================================
   126→
   127→class SetupStatus(BaseModel):
   128→    """System setup status."""
   129→    claude_cli: bool
   130→    credentials: bool
   131→    node: bool
   132→    npm: bool
   133→
   134→
   135→# ============================================================================
   136→# WebSocket Message Schemas
   137→# ============================================================================
   138→
   139→class WSProgressMessage(BaseModel):
   140→    """WebSocket message for progress updates."""
   141→    type: Literal["progress"] = "progress"
   142→    passing: int
   143→    total: int
   144→    percentage: float
   145→
   146→
   147→class WSFeatureUpdateMessage(BaseModel):
   148→    """WebSocket message for feature status updates."""
   149→    type: Literal["feature_update"] = "feature_update"
   150→    feature_id: int
   151→    passes: bool
   152→
   153→
   154→class WSLogMessage(BaseModel):
   155→    """WebSocket message for agent log output."""
   156→    type: Literal["log"] = "log"
   157→    line: str
   158→    timestamp: datetime
   159→
   160→
   161→class WSAgentStatusMessage(BaseModel):
   162→    """WebSocket message for agent status changes."""
   163→    type: Literal["agent_status"] = "agent_status"
   164→    status: str
   165→
   166→
   167→# ============================================================================
   168→# Spec Chat Schemas
   169→# ============================================================================
   170→
   171→# Maximum image file size: 5 MB
   172→MAX_IMAGE_SIZE = 5 * 1024 * 1024
   173→
   174→
   175→class ImageAttachment(BaseModel):
   176→    """Image attachment from client for spec creation chat."""
   177→    filename: str = Field(..., min_length=1, max_length=255)
   178→    mimeType: Literal['image/jpeg', 'image/png']
   179→    base64Data: str
   180→
   181→    @field_validator('base64Data')
   182→    @classmethod
   183→    def validate_base64_and_size(cls, v: str) -> str:
   184→        """Validate that base64 data is valid and within size limit."""
   185→        try:
   186→            decoded = base64.b64decode(v)
   187→            if len(decoded) > MAX_IMAGE_SIZE:
   188→                raise ValueError(
   189→                    f'Image size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
   190→                    f'maximum of {MAX_IMAGE_SIZE // (1024 * 1024)} MB'
   191→                )
   192→            return v
   193→        except Exception as e:
   194→            if 'Image size' in str(e):
   195→                raise
   196→            raise ValueError(f'Invalid base64 data: {e}')
   197→
   198→
   199→# ============================================================================
   200→# Filesystem Schemas
   201→# ============================================================================
   202→
   203→class DriveInfo(BaseModel):
   204→    """Information about a drive (Windows only)."""
   205→    letter: str
   206→    label: str
   207→    available: bool = True
   208→
   209→
   210→class DirectoryEntry(BaseModel):
   211→    """An entry in a directory listing."""
   212→    name: str
   213→    path: str  # POSIX format
   214→    is_directory: bool
   215→    is_hidden: bool = False
   216→    size: int | None = None  # Bytes, for files
   217→    has_children: bool = False  # True if directory has subdirectories
   218→
   219→
   220→class DirectoryListResponse(BaseModel):
   221→    """Response for directory listing."""
   222→    current_path: str  # POSIX format
   223→    parent_path: str | None
   224→    entries: list[DirectoryEntry]
   225→    drives: list[DriveInfo] | None = None  # Windows only
   226→
   227→
   228→class PathValidationResponse(BaseModel):
   229→    """Response for path validation."""
   230→    valid: bool
   231→    exists: bool
   232→    is_directory: bool
   233→    can_read: bool
   234→    can_write: bool
   235→    message: str = ""
   236→
   237→
   238→class CreateDirectoryRequest(BaseModel):
   239→    """Request to create a new directory."""
   240→    parent_path: str
   241→    name: str = Field(..., min_length=1, max_length=255)
   242→

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

"""
Pydantic Schemas
================

Request/Response models for the API endpoints.
"""

import base64
from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field, field_validator

# ============================================================================
# Project Schemas
# ============================================================================

class ProjectCreate(BaseModel):
    """Request schema for creating a new project."""
    name: str = Field(..., min_length=1, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
    path: str = Field(..., min_length=1, description="Absolute path to project directory")
    spec_method: Literal["claude", "manual"] = "claude"

class ProjectStats(BaseModel):
    """Project statistics."""
    passing: int = 0
    in_progress: int = 0
    total: int = 0
    percentage: float = 0.0

class ProjectSummary(BaseModel):
    """Summary of a project for list view."""
    name: str
    path: str
    has_spec: bool
    stats: ProjectStats

class ProjectDetail(BaseModel):
    """Detailed project information."""
    name: str
    path: str
    has_spec: bool
    stats: ProjectStats
    prompts_dir: str

class ProjectPrompts(BaseModel):
    """Project prompt files content."""
    app_spec: str = ""
    initializer_prompt: str = ""
    coding_prompt: str = ""

class ProjectPromptsUpdate(BaseModel):
    """Request schema for updating project prompts."""
    app_spec: str | None = None
    initializer_prompt: str | None = None
    coding_prompt: str | None = None

# ============================================================================
# Feature Schemas
# ============================================================================

class FeatureBase(BaseModel):
    """Base feature attributes."""
    category: str
    name: str
    description: str
    steps: list[str]

class FeatureCreate(FeatureBase):
    """Request schema for creating a new feature."""
    priority: int | None = None

class FeatureResponse(FeatureBase):
    """Response schema for a feature."""
    id: int
    priority: int
    passes: bool
    in_progress: bool

    class Config:
        from_attributes = True

class FeatureListResponse(BaseModel):
    """Response containing list of features organized by status."""
    pending: list[FeatureResponse]
    in_progress: list[FeatureResponse]
    done: list[FeatureResponse]

# ============================================================================
# Agent Schemas
# ============================================================================

class AgentStartRequest(BaseModel):
    """Request schema for starting the agent."""
    yolo_mode: bool = False

class AgentStatus(BaseModel):
    """Current agent status."""
    status: Literal["stopped", "running", "paused", "crashed"]
    pid: int | None = None
    started_at: datetime | None = None
    yolo_mode: bool = False

class AgentActionResponse(BaseModel):
    """Response for agent control actions."""
    success: bool
    status: str
    message: str = ""

# ============================================================================
# Setup Schemas
# ============================================================================

class SetupStatus(BaseModel):
    """System setup status."""
    claude_cli: bool
    credentials: bool
    node: bool
    npm: bool

# ============================================================================
# WebSocket Message Schemas
# ============================================================================

class WSProgressMessage(BaseModel):
    """WebSocket message for progress updates."""
    type: Literal["progress"] = "progress"
    passing: int
    total: int
    percentage: float

class WSFeatureUpdateMessage(BaseModel):
    """WebSocket message for feature status updates."""
    type: Literal["feature_update"] = "feature_update"
    feature_id: int
    passes: bool

class WSLogMessage(BaseModel):
    """WebSocket message for agent log output."""
    type: Literal["log"] = "log"
    line: str
    timestamp: datetime

class WSAgentStatusMessage(BaseModel):
    """WebSocket message for agent status changes."""
    type: Literal["agent_status"] = "agent_status"
    status: str

# ============================================================================
# Spec Chat Schemas
# ============================================================================

# Maximum image file size: 5 MB
MAX_IMAGE_SIZE = 5 * 1024 * 1024

class ImageAttachment(BaseModel):
    """Image attachment from client for spec creation chat."""
    filename: str = Field(..., min_length=1, max_length=255)
    mimeType: Literal['image/jpeg', 'image/png']
    base64Data: str

    @field_validator('base64Data')
    @classmethod
    def validate_base64_and_size(cls, v: str) -> str:
        """Validate that base64 data is valid and within size limit."""
        try:
            decoded = base64.b64decode(v)
            if len(decoded) > MAX_IMAGE_SIZE:
                raise ValueError(
                    f'Image size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
                    f'maximum of {MAX_IMAGE_SIZE // (1024 * 1024)} MB'
                )
            return v
        except Exception as e:
            if 'Image size' in str(e):
                raise
            raise ValueError(f'Invalid base64 data: {e}')

# ============================================================================
# Filesystem Schemas
# ============================================================================

class DriveInfo(BaseModel):
    """Information about a drive (Windows only)."""
    letter: str
    label: str
    available: bool = True

class DirectoryEntry(BaseModel):
    """An entry in a directory listing."""
    name: str
    path: str  # POSIX format
    is_directory: bool
    is_hidden: bool = False
    size: int | None = None  # Bytes, for files
    has_children: bool = False  # True if directory has subdirectories

class DirectoryListResponse(BaseModel):
    """Response for directory listing."""
    current_path: str  # POSIX format
    parent_path: str | None
    entries: list[DirectoryEntry]
    drives: list[DriveInfo] | None = None  # Windows only

class PathValidationResponse(BaseModel):
    """Response for path validation."""
    valid: bool
    exists: bool
    is_directory: bool
    can_read: bool
    can_write: bool
    message: str = ""

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str = Field(..., min_length=1, max_length=255)


Good, now I can add the reference folder schemas to this file, and then add the API endpoints.


class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str = Field(..., min_length=1, max_length=255)

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str = Field(..., min_length=1, max_length=255)

# ============================================================================
# Reference Folder Schemas
# ============================================================================

class ReferenceFolderCreate(BaseModel):
    """Request schema for adding a reference folder."""
    path: str = Field(..., min_length=1, description="Absolute path to reference folder")
    alias: str | None = Field(None, max_length=100, description="Optional friendly name")
    description: str | None = Field(None, max_length=500, description="What this reference is for")

class ReferenceFolderUpdate(BaseModel):
    """Request schema for updating a reference folder."""
    alias: str | None = Field(None, max_length=100)
    description: str | None = Field(None, max_length=500)

class ReferenceFolderResponse(BaseModel):
    """Response schema for a reference folder."""
    id: int
    path: str
    alias: str | None
    description: str | None
    created_at: str | None
    exists: bool  # Whether the path still exists on disk


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\schemas.py has been updated successfully.

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str = Field(..., min_length=1, max_length=255)

# ============================================================================
# Reference Folder Schemas
# ============================================================================

class ReferenceFolderCreate(BaseModel):
    """Request schema for adding a reference folder."""
    path: str = Field(..., min_length=1, description="Absolute path to reference folder")
    alias: str | None = Field(None, max_length=100, description="Optional friendly name")
    description: str | None = Field(None, max_length=500, description="What this reference is for")

class ReferenceFolderUpdate(BaseModel):
    """Request schema for updating a reference folder."""
    alias: str | None = Field(None, max_length=100)
    description: str | None = Field(None, max_length=500)

class ReferenceFolderResponse(BaseModel):
    """Response schema for a reference folder."""
    id: int
    path: str
    alias: str | None
    description: str | None
    created_at: str | None
    exists: bool  # Whether the path still exists on disk

"""
Pydantic Schemas
================

Request/Response models for the API endpoints.
"""

import base64
from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field, field_validator

# ============================================================================
# Project Schemas
# ============================================================================

class ProjectCreate(BaseModel):
    """Request schema for creating a new project."""
    name: str = Field(..., min_length=1, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
    path: str = Field(..., min_length=1, description="Absolute path to project directory")
    spec_method: Literal["claude", "manual"] = "claude"

class ProjectStats(BaseModel):
    """Project statistics."""
    passing: int = 0
    in_progress: int = 0
    total: int = 0
    percentage: float = 0.0

class ProjectSummary(BaseModel):
    """Summary of a project for list view."""
    name: str
    path: str
    has_spec: bool
    stats: ProjectStats

class ProjectDetail(BaseModel):
    """Detailed project information."""
    name: str
    path: str
    has_spec: bool
    stats: ProjectStats
    prompts_dir: str

class ProjectPrompts(BaseModel):
    """Project prompt files content."""
    app_spec: str = ""
    initializer_prompt: str = ""
    coding_prompt: str = ""

class ProjectPromptsUpdate(BaseModel):
    """Request schema for updating project prompts."""
    app_spec: str | None = None
    initializer_prompt: str | None = None
    coding_prompt: str | None = None

# ============================================================================
# Feature Schemas
# ============================================================================

class FeatureBase(BaseModel):
    """Base feature attributes."""
    category: str
    name: str
    description: str
    steps: list[str]

class FeatureCreate(FeatureBase):
    """Request schema for creating a new feature."""
    priority: int | None = None

class FeatureResponse(FeatureBase):
    """Response schema for a feature."""
    id: int
    priority: int
    passes: bool
    in_progress: bool

    class Config:
        from_attributes = True

class FeatureListResponse(BaseModel):
    """Response containing list of features organized by status."""
    pending: list[FeatureResponse]
    in_progress: list[FeatureResponse]
    done: list[FeatureResponse]

# ============================================================================
# Agent Schemas
# ============================================================================

class AgentStartRequest(BaseModel):
    """Request schema for starting the agent."""
    yolo_mode: bool = False

class AgentStatus(BaseModel):
    """Current agent status."""
    status: Literal["stopped", "running", "paused", "crashed"]
    pid: int | None = None
    started_at: datetime | None = None
    yolo_mode: bool = False

class AgentActionResponse(BaseModel):
    """Response for agent control actions."""
    success: bool
    status: str
    message: str = ""

# ============================================================================
# Setup Schemas
# ============================================================================

class SetupStatus(BaseModel):
    """System setup status."""
    claude_cli: bool
    credentials: bool
    node: bool
    npm: bool

# ============================================================================
# WebSocket Message Schemas
# ============================================================================

class WSProgressMessage(BaseModel):
    """WebSocket message for progress updates."""
    type: Literal["progress"] = "progress"
    passing: int
    total: int
    percentage: float

class WSFeatureUpdateMessage(BaseModel):
    """WebSocket message for feature status updates."""
    type: Literal["feature_update"] = "feature_update"
    feature_id: int
    passes: bool

class WSLogMessage(BaseModel):
    """WebSocket message for agent log output."""
    type: Literal["log"] = "log"
    line: str
    timestamp: datetime

class WSAgentStatusMessage(BaseModel):
    """WebSocket message for agent status changes."""
    type: Literal["agent_status"] = "agent_status"
    status: str

# ============================================================================
# Spec Chat Schemas
# ============================================================================

# Maximum image file size: 5 MB
MAX_IMAGE_SIZE = 5 * 1024 * 1024

class ImageAttachment(BaseModel):
    """Image attachment from client for spec creation chat."""
    filename: str = Field(..., min_length=1, max_length=255)
    mimeType: Literal['image/jpeg', 'image/png']
    base64Data: str

    @field_validator('base64Data')
    @classmethod
    def validate_base64_and_size(cls, v: str) -> str:
        """Validate that base64 data is valid and within size limit."""
        try:
            decoded = base64.b64decode(v)
            if len(decoded) > MAX_IMAGE_SIZE:
                raise ValueError(
                    f'Image size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
                    f'maximum of {MAX_IMAGE_SIZE // (1024 * 1024)} MB'
                )
            return v
        except Exception as e:
            if 'Image size' in str(e):
                raise
            raise ValueError(f'Invalid base64 data: {e}')

# ============================================================================
# Filesystem Schemas
# ============================================================================

class DriveInfo(BaseModel):
    """Information about a drive (Windows only)."""
    letter: str
    label: str
    available: bool = True

class DirectoryEntry(BaseModel):
    """An entry in a directory listing."""
    name: str
    path: str  # POSIX format
    is_directory: bool
    is_hidden: bool = False
    size: int | None = None  # Bytes, for files
    has_children: bool = False  # True if directory has subdirectories

class DirectoryListResponse(BaseModel):
    """Response for directory listing."""
    current_path: str  # POSIX format
    parent_path: str | None
    entries: list[DirectoryEntry]
    drives: list[DriveInfo] | None = None  # Windows only

class PathValidationResponse(BaseModel):
    """Response for path validation."""
    valid: bool
    exists: bool
    is_directory: bool
    can_read: bool
    can_write: bool
    message: str = ""

class CreateDirectoryRequest(BaseModel):
    """Request to create a new directory."""
    parent_path: str
    name: str = Field(..., min_length=1, max_length=255)

+    """Request schema for adding a reference folder."""

+    path: str = Field(..., min_length=1, description="Absolute path to reference folder")

+    alias: str | None = Field(None, max_length=100, description="Optional friendly name")

+    description: str | None = Field(None, max_length=500, description="What this reference is for")

+    """Request schema for updating a reference folder."""

+    alias: str | None = Field(None, max_length=100)

+    description: str | None = Field(None, max_length=500)

+    """Response schema for a reference folder."""

+    exists: bool  # Whether the path still exists on disk


Now I need to add the API endpoints to the projects router. Let me add them at the end of the file.


   320→
   321→    if not project_dir:
   322→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   323→
   324→    if not project_dir.exists():
   325→        raise HTTPException(status_code=404, detail="Project directory not found")
   326→
   327→    prompts_dir = _get_project_prompts_dir(project_dir)
   328→    prompts_dir.mkdir(parents=True, exist_ok=True)
   329→
   330→    def write_file(filename: str, content: str | None):
   331→        if content is not None:
   332→            filepath = prompts_dir / filename
   333→            filepath.write_text(content, encoding="utf-8")
   334→
   335→    write_file("app_spec.txt", prompts.app_spec)
   336→    write_file("initializer_prompt.md", prompts.initializer_prompt)
   337→    write_file("coding_prompt.md", prompts.coding_prompt)
   338→
   339→    return {"success": True, "message": "Prompts updated"}
   340→
   341→
   342→@router.get("/{name}/stats", response_model=ProjectStats)
   343→async def get_project_stats_endpoint(name: str):
   344→    """Get current progress statistics for a project."""
   345→    _init_imports()
   346→    _, _, get_project_path, _, _ = _get_registry_functions()
   347→
   348→    name = validate_project_name(name)
   349→    project_dir = get_project_path(name)

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

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)
    prompts_dir.mkdir(parents=True, exist_ok=True)

    def write_file(filename: str, content: str | None):
        if content is not None:
            filepath = prompts_dir / filename
            filepath.write_text(content, encoding="utf-8")

    write_file("app_spec.txt", prompts.app_spec)
    write_file("initializer_prompt.md", prompts.initializer_prompt)
    write_file("coding_prompt.md", prompts.coding_prompt)

    return {"success": True, "message": "Prompts updated"}

@router.get("/{name}/stats", response_model=ProjectStats)
async def get_project_stats_endpoint(name: str):
    """Get current progress statistics for a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)


   350→
   351→    if not project_dir:
   352→        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
   353→
   354→    if not project_dir.exists():
   355→        raise HTTPException(status_code=404, detail="Project directory not found")
   356→
   357→    return get_project_stats(project_dir)
   358→

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

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)


Good, now I can add the reference folder endpoints at the end of the file.


    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)

# ============================================================================
# Reference Folder Endpoints
# ============================================================================

from ..schemas import ReferenceFolderCreate, ReferenceFolderUpdate, ReferenceFolderResponse

@router.get("/{name}/references", response_model=list[ReferenceFolderResponse])
async def list_references(name: str):
    """List all reference folders for a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, _, list_reference_folders, _, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    refs = list_reference_folders(name)
    return [ReferenceFolderResponse(**r) for r in refs]

@router.post("/{name}/references", response_model=ReferenceFolderResponse)
async def add_reference(name: str, request: ReferenceFolderCreate):
    """Add a reference folder to a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    add_reference_folder, _, list_reference_folders, get_reference_folder, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Security check: don't allow system paths
    from .filesystem import is_path_blocked
    ref_path = Path(request.path).resolve()
    if is_path_blocked(ref_path):
        raise HTTPException(
            status_code=403,
            detail="Cannot add reference to system or sensitive directory"
        )

    try:
        ref_id = add_reference_folder(
            project_name=name,
            path=ref_path,
            alias=request.alias,
            description=request.description,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=409, detail=str(e))

    ref = get_reference_folder(ref_id)
    return ReferenceFolderResponse(**ref)

@router.delete("/{name}/references/{ref_id}")
async def remove_reference(name: str, ref_id: int):
    """Remove a reference folder from a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, remove_reference_folder, _, get_reference_folder, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Verify the reference belongs to this project
    ref = get_reference_folder(ref_id)
    if not ref or ref.get("project_name") != name:
        raise HTTPException(status_code=404, detail="Reference folder not found")

    remove_reference_folder(ref_id)
    return {"success": True, "message": "Reference folder removed"}

@router.patch("/{name}/references/{ref_id}", response_model=ReferenceFolderResponse)
async def update_reference(name: str, ref_id: int, request: ReferenceFolderUpdate):
    """Update a reference folder's metadata."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, _, _, get_reference_folder, update_reference_folder = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Verify the reference belongs to this project
    ref = get_reference_folder(ref_id)
    if not ref or ref.get("project_name") != name:
        raise HTTPException(status_code=404, detail="Reference folder not found")

    update_reference_folder(
        reference_id=ref_id,
        alias=request.alias,
        description=request.description,
    )

    ref = get_reference_folder(ref_id)
    return ReferenceFolderResponse(**ref)


    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)

# ============================================================================
# Reference Folder Endpoints
# ============================================================================

from ..schemas import ReferenceFolderCreate, ReferenceFolderUpdate, ReferenceFolderResponse

@router.get("/{name}/references", response_model=list[ReferenceFolderResponse])
async def list_references(name: str):
    """List all reference folders for a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, _, list_reference_folders, _, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    refs = list_reference_folders(name)
    return [ReferenceFolderResponse(**r) for r in refs]

@router.post("/{name}/references", response_model=ReferenceFolderResponse)
async def add_reference(name: str, request: ReferenceFolderCreate):
    """Add a reference folder to a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    add_reference_folder, _, list_reference_folders, get_reference_folder, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Security check: don't allow system paths
    from .filesystem import is_path_blocked
    ref_path = Path(request.path).resolve()
    if is_path_blocked(ref_path):
        raise HTTPException(
            status_code=403,
            detail="Cannot add reference to system or sensitive directory"
        )

    try:
        ref_id = add_reference_folder(
            project_name=name,
            path=ref_path,
            alias=request.alias,
            description=request.description,
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=409, detail=str(e))

    ref = get_reference_folder(ref_id)
    return ReferenceFolderResponse(**ref)

@router.delete("/{name}/references/{ref_id}")
async def remove_reference(name: str, ref_id: int):
    """Remove a reference folder from a project."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, remove_reference_folder, _, get_reference_folder, _ = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Verify the reference belongs to this project
    ref = get_reference_folder(ref_id)
    if not ref or ref.get("project_name") != name:
        raise HTTPException(status_code=404, detail="Reference folder not found")

    remove_reference_folder(ref_id)
    return {"success": True, "message": "Reference folder removed"}

@router.patch("/{name}/references/{ref_id}", response_model=ReferenceFolderResponse)
async def update_reference(name: str, ref_id: int, request: ReferenceFolderUpdate):
    """Update a reference folder's metadata."""
    _, _, get_project_path, _, _ = _get_registry_functions()
    _, _, _, get_reference_folder, update_reference_folder = _get_reference_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Verify the reference belongs to this project
    ref = get_reference_folder(ref_id)
    if not ref or ref.get("project_name") != name:
        raise HTTPException(status_code=404, detail="Reference folder not found")

    update_reference_folder(
        reference_id=ref_id,
        alias=request.alias,
        description=request.description,
    )

    ref = get_reference_folder(ref_id)
    return ReferenceFolderResponse(**ref)

"""
Projects Router
===============

API endpoints for project management.
Uses project registry for path lookups instead of fixed generations/ directory.
"""

import re
import shutil
from pathlib import Path

from fastapi import APIRouter, HTTPException

from ..schemas import (
    ProjectCreate,
    ProjectDetail,
    ProjectPrompts,
    ProjectPromptsUpdate,
    ProjectStats,
    ProjectSummary,
)

# Lazy imports to avoid circular dependencies
_imports_initialized = False
_check_spec_exists = None
_scaffold_project_prompts = None
_get_project_prompts_dir = None
_count_passing_tests = None

def _init_imports():
    """Lazy import of project-level modules."""
    global _imports_initialized, _check_spec_exists
    global _scaffold_project_prompts, _get_project_prompts_dir
    global _count_passing_tests

    if _imports_initialized:
        return

    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from progress import count_passing_tests
    from prompts import get_project_prompts_dir, scaffold_project_prompts
    from start import check_spec_exists

    _check_spec_exists = check_spec_exists
    _scaffold_project_prompts = scaffold_project_prompts
    _get_project_prompts_dir = get_project_prompts_dir
    _count_passing_tests = count_passing_tests
    _imports_initialized = True

def _get_registry_functions():
    """Get registry functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        get_project_path,
        list_registered_projects,
        register_project,
        unregister_project,
        validate_project_path,
    )
    return register_project, unregister_project, get_project_path, list_registered_projects, validate_project_path

def _get_reference_functions():
    """Get reference folder functions with lazy import."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import (
        add_reference_folder,
        remove_reference_folder,
        list_reference_folders,
        get_reference_folder,
        update_reference_folder,
    )
    return add_reference_folder, remove_reference_folder, list_reference_folders, get_reference_folder, update_reference_folder

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

def validate_project_name(name: str) -> str:
    """Validate and sanitize project name to prevent path traversal."""
    if not re.match(r'^[a-zA-Z0-9_-]{1,50}$', name):
        raise HTTPException(
            status_code=400,
            detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
        )
    return name

def get_project_stats(project_dir: Path) -> ProjectStats:
    """Get statistics for a project."""
    _init_imports()
    passing, in_progress, total = _count_passing_tests(project_dir)
    percentage = (passing / total * 100) if total > 0 else 0.0
    return ProjectStats(
        passing=passing,
        in_progress=in_progress,
        total=total,
        percentage=round(percentage, 1)
    )

@router.get("", response_model=list[ProjectSummary])
async def list_projects():
    """List all registered projects."""
    _init_imports()
    _, _, _, list_registered_projects, validate_project_path = _get_registry_functions()

    projects = list_registered_projects()
    result = []

    for name, info in projects.items():
        project_dir = Path(info["path"])

        # Skip if path no longer exists
        is_valid, _ = validate_project_path(project_dir)
        if not is_valid:
            continue

        has_spec = _check_spec_exists(project_dir)
        stats = get_project_stats(project_dir)

        result.append(ProjectSummary(
            name=name,
            path=info["path"],
            has_spec=has_spec,
            stats=stats,
        ))

    return result

@router.post("", response_model=ProjectSummary)
async def create_project(project: ProjectCreate):
    """Create a new project at the specified path."""
    _init_imports()
    register_project, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(project.name)
    project_path = Path(project.path).resolve()

    # Check if project name already registered
    existing = get_project_path(name)
    if existing:
        raise HTTPException(
            status_code=409,
            detail=f"Project '{name}' already exists at {existing}"
        )

    # Security: Check if path is in a blocked location
    from .filesystem import is_path_blocked
    if is_path_blocked(project_path):
        raise HTTPException(
            status_code=403,
            detail="Cannot create project in system or sensitive directory"
        )

    # Validate the path is usable
    if project_path.exists():
        if not project_path.is_dir():
            raise HTTPException(
                status_code=400,
                detail="Path exists but is not a directory"
            )
    else:
        # Create the directory
        try:
            project_path.mkdir(parents=True, exist_ok=True)
        except OSError as e:
            raise HTTPException(
                status_code=500,
                detail=f"Failed to create directory: {e}"
            )

    # Scaffold prompts
    _scaffold_project_prompts(project_path)

    # Register in registry
    try:
        register_project(name, project_path)
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Failed to register project: {e}"
        )

    return ProjectSummary(
        name=name,
        path=project_path.as_posix(),
        has_spec=False,  # Just created, no spec yet
        stats=ProjectStats(passing=0, total=0, percentage=0.0),
    )

@router.get("/{name}", response_model=ProjectDetail)
async def get_project(name: str):
    """Get detailed information about a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found in registry")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail=f"Project directory no longer exists: {project_dir}")

    has_spec = _check_spec_exists(project_dir)
    stats = get_project_stats(project_dir)
    prompts_dir = _get_project_prompts_dir(project_dir)

    return ProjectDetail(
        name=name,
        path=project_dir.as_posix(),
        has_spec=has_spec,
        stats=stats,
        prompts_dir=str(prompts_dir),
    )

@router.delete("/{name}")
async def delete_project(name: str, delete_files: bool = False):
    """
    Delete a project from the registry.

    Args:
        name: Project name to delete
        delete_files: If True, also delete the project directory and files
    """
    _init_imports()
    _, unregister_project, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    # Check if agent is running
    lock_file = project_dir / ".agent.lock"
    if lock_file.exists():
        raise HTTPException(
            status_code=409,
            detail="Cannot delete project while agent is running. Stop the agent first."
        )

    # Optionally delete files
    if delete_files and project_dir.exists():
        try:
            shutil.rmtree(project_dir)
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Failed to delete project files: {e}")

    # Unregister from registry
    unregister_project(name)

    return {
        "success": True,
        "message": f"Project '{name}' deleted" + (" (files removed)" if delete_files else " (files preserved)")
    }

@router.get("/{name}/prompts", response_model=ProjectPrompts)
async def get_project_prompts(name: str):
    """Get the content of project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)

    def read_file(filename: str) -> str:
        filepath = prompts_dir / filename
        if filepath.exists():
            try:
                return filepath.read_text(encoding="utf-8")
            except Exception:
                return ""
        return ""

    return ProjectPrompts(
        app_spec=read_file("app_spec.txt"),
        initializer_prompt=read_file("initializer_prompt.md"),
        coding_prompt=read_file("coding_prompt.md"),
    )

@router.put("/{name}/prompts")
async def update_project_prompts(name: str, prompts: ProjectPromptsUpdate):
    """Update project prompt files."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    prompts_dir = _get_project_prompts_dir(project_dir)
    prompts_dir.mkdir(parents=True, exist_ok=True)

    def write_file(filename: str, content: str | None):
        if content is not None:
            filepath = prompts_dir / filename
            filepath.write_text(content, encoding="utf-8")

    write_file("app_spec.txt", prompts.app_spec)
    write_file("initializer_prompt.md", prompts.initializer_prompt)
    write_file("coding_prompt.md", prompts.coding_prompt)

    return {"success": True, "message": "Prompts updated"}

@router.get("/{name}/stats", response_model=ProjectStats)
async def get_project_stats_endpoint(name: str):
    """Get current progress statistics for a project."""
    _init_imports()
    _, _, get_project_path, _, _ = _get_registry_functions()

    name = validate_project_name(name)
    project_dir = get_project_path(name)

    if not project_dir:
        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

    if not project_dir.exists():
        raise HTTPException(status_code=404, detail="Project directory not found")

    return get_project_stats(project_dir)

+from ..schemas import ReferenceFolderCreate, ReferenceFolderUpdate, ReferenceFolderResponse

+    """List all reference folders for a project."""

+    _, _, get_project_path, _, _ = _get_registry_functions()

+    _, _, list_reference_folders, _, _ = _get_reference_functions()

+    project_dir = get_project_path(name)

+        raise HTTPException(status_code=404, detail=f"Project '{name}' not found")

+    refs = list_reference_folders(name)

+    return [ReferenceFolderResponse(**r) for r in refs]

+async def add_reference(name: str, request: ReferenceFolderCreate):

+    """Add a reference folder to a project."""

+    add_reference_folder, _, list_reference_folders, get_reference_folder, _ = _get_reference_functions()

+    # Security check: don't allow system paths

+    from .filesystem import is_path_blocked

+    ref_path = Path(request.path).resolve()

+            detail="Cannot add reference to system or sensitive directory"

+            description=request.description,

+        raise HTTPException(status_code=400, detail=str(e))

+        raise HTTPException(status_code=409, detail=str(e))

+    return ReferenceFolderResponse(**ref)

+async def remove_reference(name: str, ref_id: int):

+    """Remove a reference folder from a project."""

+    _, remove_reference_folder, _, get_reference_folder, _ = _get_reference_functions()

+    project_dir = get_project_path(name)

+    # Verify the reference belongs to this project

+    if not ref or ref.get("project_name") != name:

+        raise HTTPException(status_code=404, detail="Reference folder not found")

+    return {"success": True, "message": "Reference folder removed"}

+async def update_reference(name: str, ref_id: int, request: ReferenceFolderUpdate):

+    """Update a reference folder's metadata."""

+    _, _, _, get_reference_folder, update_reference_folder = _get_reference_functions()

+        description=request.description,


Now I need to:
1. Update prompts.py to inject reference folders into agent prompts
2. Create the UI for managing reference folders

Let me update the todo list and work on prompt injection.


TodoWrite

Add reference_folders table to registry database

Adding reference_folders table to registry

Create API endpoints for reference folder management

Creating API endpoints for reference folders

Inject reference folders into agent prompts

Injecting reference folders into agent prompts

Create UI for managing reference folders

Creating UI for reference folder management

Build and test


Build and test

Build and test


Now let me update prompts.py to inject reference folder information into the agent prompts.