     1→"""
     2→JSON to SQLite Migration
     3→========================
     4→
     5→Automatically migrates existing feature_list.json files to SQLite database.
     6→"""
     7→
     8→import json
     9→import shutil
    10→from datetime import datetime
    11→from pathlib import Path
    12→from typing import Optional
    13→
    14→from sqlalchemy.orm import Session, sessionmaker
    15→
    16→from api.database import Feature
    17→
    18→
    19→def migrate_json_to_sqlite(
    20→    project_dir: Path,
    21→    session_maker: sessionmaker,
    22→) -> bool:
    23→    """
    24→    Detect existing feature_list.json, import to SQLite, rename to backup.
    25→
    26→    This function:
    27→    1. Checks if feature_list.json exists
    28→    2. Checks if database already has data (skips if so)
    29→    3. Imports all features from JSON
    30→    4. Renames JSON file to feature_list.json.backup.<timestamp>
    31→
    32→    Args:
    33→        project_dir: Directory containing the project
    34→        session_maker: SQLAlchemy session maker
    35→
    36→    Returns:
    37→        True if migration was performed, False if skipped
    38→    """
    39→    json_file = project_dir / "feature_list.json"
    40→
    41→    if not json_file.exists():
    42→        return False  # No JSON file to migrate
    43→
    44→    # Check if database already has data
    45→    session: Session = session_maker()
    46→    try:
    47→        existing_count = session.query(Feature).count()
    48→        if existing_count > 0:
    49→            print(
    50→                f"Database already has {existing_count} features, skipping migration"
    51→            )
    52→            return False
    53→    finally:
    54→        session.close()
    55→
    56→    # Load JSON data
    57→    try:
    58→        with open(json_file, "r", encoding="utf-8") as f:
    59→            features_data = json.load(f)
    60→    except json.JSONDecodeError as e:
    61→        print(f"Error parsing feature_list.json: {e}")
    62→        return False
    63→    except IOError as e:
    64→        print(f"Error reading feature_list.json: {e}")
    65→        return False
    66→
    67→    if not isinstance(features_data, list):
    68→        print("Error: feature_list.json must contain a JSON array")
    69→        return False
    70→
    71→    # Import features into database
    72→    session = session_maker()
    73→    try:
    74→        imported_count = 0
    75→        for i, feature_dict in enumerate(features_data):
    76→            # Handle both old format (no id/priority/name) and new format
    77→            feature = Feature(
    78→                id=feature_dict.get("id", i + 1),
    79→                priority=feature_dict.get("priority", i + 1),
    80→                category=feature_dict.get("category", "uncategorized"),
    81→                name=feature_dict.get("name", f"Feature {i + 1}"),
    82→                description=feature_dict.get("description", ""),
    83→                steps=feature_dict.get("steps", []),
    84→                passes=feature_dict.get("passes", False),
    85→            )
    86→            session.add(feature)
    87→            imported_count += 1
    88→
    89→        session.commit()
    90→
    91→        # Verify import
    92→        final_count = session.query(Feature).count()
    93→        print(f"Migrated {final_count} features from JSON to SQLite")
    94→
    95→    except Exception as e:
    96→        session.rollback()
    97→        print(f"Error during migration: {e}")
    98→        return False
    99→    finally:
   100→        session.close()
   101→
   102→    # Rename JSON file to backup
   103→    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
   104→    backup_file = project_dir / f"feature_list.json.backup.{timestamp}"
   105→
   106→    try:
   107→        shutil.move(json_file, backup_file)
   108→        print(f"Original JSON backed up to: {backup_file.name}")
   109→    except IOError as e:
   110→        print(f"Warning: Could not backup JSON file: {e}")
   111→        # Continue anyway - the data is in the database
   112→
   113→    return True
   114→
   115→
   116→def export_to_json(
   117→    project_dir: Path,
   118→    session_maker: sessionmaker,
   119→    output_file: Optional[Path] = None,
   120→) -> Path:
   121→    """
   122→    Export features from database back to JSON format.
   123→
   124→    Useful for debugging or if you need to revert to the old format.
   125→
   126→    Args:
   127→        project_dir: Directory containing the project
   128→        session_maker: SQLAlchemy session maker
   129→        output_file: Output file path (default: feature_list_export.json)
   130→
   131→    Returns:
   132→        Path to the exported file
   133→    """
   134→    if output_file is None:
   135→        output_file = project_dir / "feature_list_export.json"
   136→
   137→    session: Session = session_maker()
   138→    try:
   139→        features = (
   140→            session.query(Feature)
   141→            .order_by(Feature.priority.asc(), Feature.id.asc())
   142→            .all()
   143→        )
   144→
   145→        features_data = [f.to_dict() for f in features]
   146→
   147→        with open(output_file, "w", encoding="utf-8") as f:
   148→            json.dump(features_data, f, indent=2)
   149→
   150→        print(f"Exported {len(features_data)} features to {output_file}")
   151→        return output_file
   152→
   153→    finally:
   154→        session.close()
   155→

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

"""
JSON to SQLite Migration
========================

Automatically migrates existing feature_list.json files to SQLite database.
"""

import json
import shutil
from datetime import datetime
from pathlib import Path
from typing import Optional

from sqlalchemy.orm import Session, sessionmaker

from api.database import Feature

def migrate_json_to_sqlite(
    project_dir: Path,
    session_maker: sessionmaker,
) -> bool:
    """
    Detect existing feature_list.json, import to SQLite, rename to backup.

    This function:
    1. Checks if feature_list.json exists
    2. Checks if database already has data (skips if so)
    3. Imports all features from JSON
    4. Renames JSON file to feature_list.json.backup.<timestamp>

    Args:
        project_dir: Directory containing the project
        session_maker: SQLAlchemy session maker

    Returns:
        True if migration was performed, False if skipped
    """
    json_file = project_dir / "feature_list.json"

    if not json_file.exists():
        return False  # No JSON file to migrate

    # Check if database already has data
    session: Session = session_maker()
    try:
        existing_count = session.query(Feature).count()
        if existing_count > 0:
            print(
                f"Database already has {existing_count} features, skipping migration"
            )
            return False
    finally:
        session.close()

    # Load JSON data
    try:
        with open(json_file, "r", encoding="utf-8") as f:
            features_data = json.load(f)
    except json.JSONDecodeError as e:
        print(f"Error parsing feature_list.json: {e}")
        return False
    except IOError as e:
        print(f"Error reading feature_list.json: {e}")
        return False

    if not isinstance(features_data, list):
        print("Error: feature_list.json must contain a JSON array")
        return False

    # Import features into database
    session = session_maker()
    try:
        imported_count = 0
        for i, feature_dict in enumerate(features_data):
            # Handle both old format (no id/priority/name) and new format
            feature = Feature(
                id=feature_dict.get("id", i + 1),
                priority=feature_dict.get("priority", i + 1),
                category=feature_dict.get("category", "uncategorized"),
                name=feature_dict.get("name", f"Feature {i + 1}"),
                description=feature_dict.get("description", ""),
                steps=feature_dict.get("steps", []),
                passes=feature_dict.get("passes", False),
            )
            session.add(feature)
            imported_count += 1

        session.commit()

        # Verify import
        final_count = session.query(Feature).count()
        print(f"Migrated {final_count} features from JSON to SQLite")

    except Exception as e:
        session.rollback()
        print(f"Error during migration: {e}")
        return False
    finally:
        session.close()

    # Rename JSON file to backup
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_file = project_dir / f"feature_list.json.backup.{timestamp}"

    try:
        shutil.move(json_file, backup_file)
        print(f"Original JSON backed up to: {backup_file.name}")
    except IOError as e:
        print(f"Warning: Could not backup JSON file: {e}")
        # Continue anyway - the data is in the database

    return True

def export_to_json(
    project_dir: Path,
    session_maker: sessionmaker,
    output_file: Optional[Path] = None,
) -> Path:
    """
    Export features from database back to JSON format.

    Useful for debugging or if you need to revert to the old format.

    Args:
        project_dir: Directory containing the project
        session_maker: SQLAlchemy session maker
        output_file: Output file path (default: feature_list_export.json)

    Returns:
        Path to the exported file
    """
    if output_file is None:
        output_file = project_dir / "feature_list_export.json"

    session: Session = session_maker()
    try:
        features = (
            session.query(Feature)
            .order_by(Feature.priority.asc(), Feature.id.asc())
            .all()
        )

        features_data = [f.to_dict() for f in features]

        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(features_data, f, indent=2)

        print(f"Exported {len(features_data)} features to {output_file}")
        return output_file

    finally:
        session.close()


Let me check for any existing conversation/message history patterns and examine how the agent uses the SDK:


     1→#!/usr/bin/env python3
     2→"""
     3→Autonomous Coding Agent Demo
     4→============================
     5→
     6→A minimal harness demonstrating long-running autonomous coding with Claude.
     7→This script implements the two-agent pattern (initializer + coding agent) and
     8→incorporates all the strategies from the long-running agents guide.
     9→
    10→Example Usage:
    11→    # Using absolute path directly
    12→    python autonomous_agent_demo.py --project-dir C:/Projects/my-app
    13→
    14→    # Using registered project name (looked up from registry)
    15→    python autonomous_agent_demo.py --project-dir my-app
    16→
    17→    # Limit iterations for testing
    18→    python autonomous_agent_demo.py --project-dir my-app --max-iterations 5
    19→
    20→    # YOLO mode: rapid prototyping without browser testing
    21→    python autonomous_agent_demo.py --project-dir my-app --yolo
    22→"""
    23→
    24→import argparse
    25→import asyncio
    26→from pathlib import Path
    27→
    28→from dotenv import load_dotenv
    29→
    30→# Load environment variables from .env file (if it exists)
    31→# IMPORTANT: Must be called BEFORE importing other modules that read env vars at load time
    32→load_dotenv()
    33→
    34→from agent import run_autonomous_agent
    35→from registry import get_project_path
    36→
    37→# Configuration
    38→# DEFAULT_MODEL = "claude-sonnet-4-5-20250929"
    39→DEFAULT_MODEL = "claude-opus-4-5-20251101"
    40→
    41→
    42→def parse_args() -> argparse.Namespace:
    43→    """Parse command line arguments."""
    44→    parser = argparse.ArgumentParser(
    45→        description="Autonomous Coding Agent Demo - Long-running agent harness",
    46→        formatter_class=argparse.RawDescriptionHelpFormatter,
    47→        epilog="""
    48→Examples:
    49→  # Use absolute path directly
    50→  python autonomous_agent_demo.py --project-dir C:/Projects/my-app
    51→
    52→  # Use registered project name (looked up from registry)
    53→  python autonomous_agent_demo.py --project-dir my-app
    54→
    55→  # Use a specific model
    56→  python autonomous_agent_demo.py --project-dir my-app --model claude-sonnet-4-5-20250929
    57→
    58→  # Limit iterations for testing
    59→  python autonomous_agent_demo.py --project-dir my-app --max-iterations 5
    60→
    61→  # YOLO mode: rapid prototyping without browser testing
    62→  python autonomous_agent_demo.py --project-dir my-app --yolo
    63→
    64→Authentication:
    65→  Uses Claude CLI credentials from ~/.claude/.credentials.json
    66→  Run 'claude login' to authenticate (handled by start.bat/start.sh)
    67→        """,
    68→    )
    69→
    70→    parser.add_argument(
    71→        "--project-dir",
    72→        type=str,
    73→        required=True,
    74→        help="Project directory path (absolute) or registered project name",
    75→    )
    76→
    77→    parser.add_argument(
    78→        "--max-iterations",
    79→        type=int,
    80→        default=None,
    81→        help="Maximum number of agent iterations (default: unlimited)",
    82→    )
    83→
    84→    parser.add_argument(
    85→        "--model",
    86→        type=str,
    87→        default=DEFAULT_MODEL,
    88→        help=f"Claude model to use (default: {DEFAULT_MODEL})",
    89→    )
    90→
    91→    parser.add_argument(
    92→        "--yolo",
    93→        action="store_true",
    94→        default=False,
    95→        help="Enable YOLO mode: rapid prototyping without browser testing",
    96→    )
    97→
    98→    return parser.parse_args()
    99→
   100→
   101→def main() -> None:
   102→    """Main entry point."""
   103→    args = parse_args()
   104→
   105→    # Note: Authentication is handled by start.bat/start.sh before this script runs.
   106→    # The Claude SDK auto-detects credentials from ~/.claude/.credentials.json
   107→
   108→    # Resolve project directory:
   109→    # 1. If absolute path, use as-is
   110→    # 2. Otherwise, look up from registry by name
   111→    project_dir_input = args.project_dir
   112→    project_dir = Path(project_dir_input)
   113→
   114→    if project_dir.is_absolute():
   115→        # Absolute path provided - use directly
   116→        if not project_dir.exists():
   117→            print(f"Error: Project directory does not exist: {project_dir}")
   118→            return
   119→    else:
   120→        # Treat as a project name - look up from registry
   121→        registered_path = get_project_path(project_dir_input)
   122→        if registered_path:
   123→            project_dir = registered_path
   124→        else:
   125→            print(f"Error: Project '{project_dir_input}' not found in registry")
   126→            print("Use an absolute path or register the project first.")
   127→            return
   128→
   129→    try:
   130→        # Run the agent (MCP server handles feature database)
   131→        asyncio.run(
   132→            run_autonomous_agent(
   133→                project_dir=project_dir,
   134→                model=args.model,
   135→                max_iterations=args.max_iterations,
   136→                yolo_mode=args.yolo,
   137→            )
   138→        )
   139→    except KeyboardInterrupt:
   140→        print("\n\nInterrupted by user")
   141→        print("To resume, run the same command again")
   142→    except Exception as e:
   143→        print(f"\nFatal error: {e}")
   144→        raise
   145→
   146→
   147→if __name__ == "__main__":
   148→    main()
   149→

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

#!/usr/bin/env python3
"""
Autonomous Coding Agent Demo
============================

A minimal harness demonstrating long-running autonomous coding with Claude.
This script implements the two-agent pattern (initializer + coding agent) and
incorporates all the strategies from the long-running agents guide.

Example Usage:
    # Using absolute path directly
    python autonomous_agent_demo.py --project-dir C:/Projects/my-app

    # Using registered project name (looked up from registry)
    python autonomous_agent_demo.py --project-dir my-app

    # Limit iterations for testing
    python autonomous_agent_demo.py --project-dir my-app --max-iterations 5

    # YOLO mode: rapid prototyping without browser testing
    python autonomous_agent_demo.py --project-dir my-app --yolo
"""

import argparse
import asyncio
from pathlib import Path

from dotenv import load_dotenv

# Load environment variables from .env file (if it exists)
# IMPORTANT: Must be called BEFORE importing other modules that read env vars at load time
load_dotenv()

from agent import run_autonomous_agent
from registry import get_project_path

# Configuration
# DEFAULT_MODEL = "claude-sonnet-4-5-20250929"
DEFAULT_MODEL = "claude-opus-4-5-20251101"

def parse_args() -> argparse.Namespace:
    """Parse command line arguments."""
    parser = argparse.ArgumentParser(
        description="Autonomous Coding Agent Demo - Long-running agent harness",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Use absolute path directly
  python autonomous_agent_demo.py --project-dir C:/Projects/my-app

  # Use registered project name (looked up from registry)
  python autonomous_agent_demo.py --project-dir my-app

  # Use a specific model
  python autonomous_agent_demo.py --project-dir my-app --model claude-sonnet-4-5-20250929

  # Limit iterations for testing
  python autonomous_agent_demo.py --project-dir my-app --max-iterations 5

  # YOLO mode: rapid prototyping without browser testing
  python autonomous_agent_demo.py --project-dir my-app --yolo

Authentication:
  Uses Claude CLI credentials from ~/.claude/.credentials.json
  Run 'claude login' to authenticate (handled by start.bat/start.sh)
        """,
    )

    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory path (absolute) or registered project name",
    )

    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum number of agent iterations (default: unlimited)",
    )

    parser.add_argument(
        "--model",
        type=str,
        default=DEFAULT_MODEL,
        help=f"Claude model to use (default: {DEFAULT_MODEL})",
    )

    parser.add_argument(
        "--yolo",
        action="store_true",
        default=False,
        help="Enable YOLO mode: rapid prototyping without browser testing",
    )

    return parser.parse_args()

def main() -> None:
    """Main entry point."""
    args = parse_args()

    # Note: Authentication is handled by start.bat/start.sh before this script runs.
    # The Claude SDK auto-detects credentials from ~/.claude/.credentials.json

    # Resolve project directory:
    # 1. If absolute path, use as-is
    # 2. Otherwise, look up from registry by name
    project_dir_input = args.project_dir
    project_dir = Path(project_dir_input)

    if project_dir.is_absolute():
        # Absolute path provided - use directly
        if not project_dir.exists():
            print(f"Error: Project directory does not exist: {project_dir}")
            return
    else:
        # Treat as a project name - look up from registry
        registered_path = get_project_path(project_dir_input)
        if registered_path:
            project_dir = registered_path
        else:
            print(f"Error: Project '{project_dir_input}' not found in registry")
            print("Use an absolute path or register the project first.")
            return

    try:
        # Run the agent (MCP server handles feature database)
        asyncio.run(
            run_autonomous_agent(
                project_dir=project_dir,
                model=args.model,
                max_iterations=args.max_iterations,
                yolo_mode=args.yolo,
            )
        )
    except KeyboardInterrupt:
        print("\n\nInterrupted by user")
        print("To resume, run the same command again")
    except Exception as e:
        print(f"\nFatal error: {e}")
        raise

if __name__ == "__main__":
    main()


Now let me check the prompts file to see what context is passed to the agent:


     1→"""
     2→Prompt Loading Utilities
     3→========================
     4→
     5→Functions for loading prompt templates with project-specific support.
     6→
     7→Fallback chain:
     8→1. Project-specific: {project_dir}/prompts/{name}.md
     9→2. Base template: .claude/templates/{name}.template.md
    10→
    11→Context injection:
    12→- Infrastructure context from ResourceManager (servers, services)
    13→- Credential context from CredentialResolver (with redacted secrets)
    14→"""
    15→
    16→import shutil
    17→from pathlib import Path
    18→from typing import TYPE_CHECKING
    19→
    20→if TYPE_CHECKING:
    21→    from server.services.resource_manager import ResourceManager
    22→    from server.services.credential_resolver import CredentialResolver
    23→
    24→# Base templates location (generic templates)
    25→TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates"
    26→
    27→
    28→def get_project_prompts_dir(project_dir: Path) -> Path:
    29→    """Get the prompts directory for a specific project."""
    30→    return project_dir / "prompts"
    31→
    32→
    33→def load_prompt(name: str, project_dir: Path | None = None) -> str:
    34→    """
    35→    Load a prompt template with fallback chain.
    36→
    37→    Fallback order:
    38→    1. Project-specific: {project_dir}/prompts/{name}.md
    39→    2. Base template: .claude/templates/{name}.template.md
    40→
    41→    Args:
    42→        name: The prompt name (without extension), e.g., "initializer_prompt"
    43→        project_dir: Optional project directory for project-specific prompts
    44→
    45→    Returns:
    46→        The prompt content as a string
    47→
    48→    Raises:
    49→        FileNotFoundError: If prompt not found in any location
    50→    """
    51→    # 1. Try project-specific first
    52→    if project_dir:
    53→        project_prompts = get_project_prompts_dir(project_dir)
    54→        project_path = project_prompts / f"{name}.md"
    55→        if project_path.exists():
    56→            try:
    57→                return project_path.read_text(encoding="utf-8")
    58→            except (OSError, PermissionError) as e:
    59→                print(f"Warning: Could not read {project_path}: {e}")
    60→
    61→    # 2. Try base template
    62→    template_path = TEMPLATES_DIR / f"{name}.template.md"
    63→    if template_path.exists():
    64→        try:
    65→            return template_path.read_text(encoding="utf-8")
    66→        except (OSError, PermissionError) as e:
    67→            print(f"Warning: Could not read {template_path}: {e}")
    68→
    69→    raise FileNotFoundError(
    70→        f"Prompt '{name}' not found in:\n"
    71→        f"  - Project: {project_dir / 'prompts' if project_dir else 'N/A'}\n"
    72→        f"  - Templates: {TEMPLATES_DIR}"
    73→    )
    74→
    75→
    76→def get_initializer_prompt(project_dir: Path | None = None) -> str:
    77→    """Load the initializer prompt (project-specific if available)."""
    78→    return load_prompt("initializer_prompt", project_dir)
    79→
    80→
    81→def get_coding_prompt(project_dir: Path | None = None) -> str:
    82→    """Load the coding agent prompt (project-specific if available)."""
    83→    return load_prompt("coding_prompt", project_dir)
    84→
    85→
    86→def get_coding_prompt_yolo(project_dir: Path | None = None) -> str:
    87→    """Load the YOLO mode coding agent prompt (project-specific if available)."""
    88→    return load_prompt("coding_prompt_yolo", project_dir)
    89→
    90→
    91→def get_app_spec(project_dir: Path) -> str:
    92→    """
    93→    Load the app spec from the project.
    94→
    95→    Checks in order:
    96→    1. Project prompts directory: {project_dir}/prompts/app_spec.txt
    97→    2. Project root (legacy): {project_dir}/app_spec.txt
    98→
    99→    Args:
   100→        project_dir: The project directory
   101→
   102→    Returns:
   103→        The app spec content
   104→
   105→    Raises:
   106→        FileNotFoundError: If no app_spec.txt found
   107→    """
   108→    # Try project prompts directory first
   109→    project_prompts = get_project_prompts_dir(project_dir)
   110→    spec_path = project_prompts / "app_spec.txt"
   111→    if spec_path.exists():
   112→        try:
   113→            return spec_path.read_text(encoding="utf-8")
   114→        except (OSError, PermissionError) as e:
   115→            raise FileNotFoundError(f"Could not read {spec_path}: {e}") from e
   116→
   117→    # Fallback to legacy location in project root
   118→    legacy_spec = project_dir / "app_spec.txt"
   119→    if legacy_spec.exists():
   120→        try:
   121→            return legacy_spec.read_text(encoding="utf-8")
   122→        except (OSError, PermissionError) as e:
   123→            raise FileNotFoundError(f"Could not read {legacy_spec}: {e}") from e
   124→
   125→    raise FileNotFoundError(f"No app_spec.txt found for project: {project_dir}")
   126→
   127→
   128→def scaffold_project_prompts(project_dir: Path) -> Path:
   129→    """
   130→    Create the project prompts directory and copy base templates.
   131→
   132→    This sets up a new project with template files that can be customized.
   133→
   134→    Args:
   135→        project_dir: The absolute path to the project directory
   136→
   137→    Returns:
   138→        The path to the project prompts directory
   139→    """
   140→    project_prompts = get_project_prompts_dir(project_dir)
   141→    project_prompts.mkdir(parents=True, exist_ok=True)
   142→
   143→    # Define template mappings: (source_template, destination_name)
   144→    templates = [
   145→        ("app_spec.template.txt", "app_spec.txt"),
   146→        ("coding_prompt.template.md", "coding_prompt.md"),
   147→        ("coding_prompt_yolo.template.md", "coding_prompt_yolo.md"),
   148→        ("initializer_prompt.template.md", "initializer_prompt.md"),
   149→    ]
   150→
   151→    copied_files = []
   152→    for template_name, dest_name in templates:
   153→        template_path = TEMPLATES_DIR / template_name
   154→        dest_path = project_prompts / dest_name
   155→
   156→        # Only copy if template exists and destination doesn't
   157→        if template_path.exists() and not dest_path.exists():
   158→            try:
   159→                shutil.copy(template_path, dest_path)
   160→                copied_files.append(dest_name)
   161→            except (OSError, PermissionError) as e:
   162→                print(f"  Warning: Could not copy {dest_name}: {e}")
   163→
   164→    if copied_files:
   165→        print(f"  Created prompt files: {', '.join(copied_files)}")
   166→
   167→    return project_prompts
   168→
   169→
   170→def has_project_prompts(project_dir: Path) -> bool:
   171→    """
   172→    Check if a project has valid prompts set up.
   173→
   174→    A project has valid prompts if:
   175→    1. The prompts directory exists, AND
   176→    2. app_spec.txt exists within it, AND
   177→    3. app_spec.txt contains the <project_specification> tag
   178→
   179→    Args:
   180→        project_dir: The project directory to check
   181→
   182→    Returns:
   183→        True if valid project prompts exist, False otherwise
   184→    """
   185→    project_prompts = get_project_prompts_dir(project_dir)
   186→    app_spec = project_prompts / "app_spec.txt"
   187→
   188→    if not app_spec.exists():
   189→        # Also check legacy location in project root
   190→        legacy_spec = project_dir / "app_spec.txt"
   191→        if legacy_spec.exists():
   192→            try:
   193→                content = legacy_spec.read_text(encoding="utf-8")
   194→                return "<project_specification>" in content
   195→            except (OSError, PermissionError):
   196→                return False
   197→        return False
   198→
   199→    # Check for valid spec content
   200→    try:
   201→        content = app_spec.read_text(encoding="utf-8")
   202→        return "<project_specification>" in content
   203→    except (OSError, PermissionError):
   204→        return False
   205→
   206→
   207→def copy_spec_to_project(project_dir: Path) -> None:
   208→    """
   209→    Copy the app spec file into the project root directory for the agent to read.
   210→
   211→    This maintains backwards compatibility - the agent expects app_spec.txt
   212→    in the project root directory.
   213→
   214→    The spec is sourced from: {project_dir}/prompts/app_spec.txt
   215→
   216→    Args:
   217→        project_dir: The project directory
   218→    """
   219→    spec_dest = project_dir / "app_spec.txt"
   220→
   221→    # Don't overwrite if already exists
   222→    if spec_dest.exists():
   223→        return
   224→
   225→    # Copy from project prompts directory
   226→    project_prompts = get_project_prompts_dir(project_dir)
   227→    project_spec = project_prompts / "app_spec.txt"
   228→    if project_spec.exists():
   229→        try:
   230→            shutil.copy(project_spec, spec_dest)
   231→            print("Copied app_spec.txt to project directory")
   232→            return
   233→        except (OSError, PermissionError) as e:
   234→            print(f"Warning: Could not copy app_spec.txt: {e}")
   235→            return
   236→
   237→    print("Warning: No app_spec.txt found to copy to project directory")
   238→
   239→
   240→# =============================================================================
   241→# Context Injection
   242→# =============================================================================
   243→
   244→
   245→def get_infrastructure_context(
   246→    project_name: str,
   247→    project_dir: Path,
   248→    include_credentials: bool = True,
   249→) -> str:
   250→    """

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

"""
Prompt Loading Utilities
========================

Functions for loading prompt templates with project-specific support.

Fallback chain:
1. Project-specific: {project_dir}/prompts/{name}.md
2. Base template: .claude/templates/{name}.template.md

Context injection:
- Infrastructure context from ResourceManager (servers, services)
- Credential context from CredentialResolver (with redacted secrets)
"""

import shutil
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from server.services.resource_manager import ResourceManager
    from server.services.credential_resolver import CredentialResolver

# Base templates location (generic templates)
TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates"

def get_project_prompts_dir(project_dir: Path) -> Path:
    """Get the prompts directory for a specific project."""
    return project_dir / "prompts"

def load_prompt(name: str, project_dir: Path | None = None) -> str:
    """
    Load a prompt template with fallback chain.

    Fallback order:
    1. Project-specific: {project_dir}/prompts/{name}.md
    2. Base template: .claude/templates/{name}.template.md

    Args:
        name: The prompt name (without extension), e.g., "initializer_prompt"
        project_dir: Optional project directory for project-specific prompts

    Returns:
        The prompt content as a string

    Raises:
        FileNotFoundError: If prompt not found in any location
    """
    # 1. Try project-specific first
    if project_dir:
        project_prompts = get_project_prompts_dir(project_dir)
        project_path = project_prompts / f"{name}.md"
        if project_path.exists():
            try:
                return project_path.read_text(encoding="utf-8")
            except (OSError, PermissionError) as e:
                print(f"Warning: Could not read {project_path}: {e}")

    # 2. Try base template
    template_path = TEMPLATES_DIR / f"{name}.template.md"
    if template_path.exists():
        try:
            return template_path.read_text(encoding="utf-8")
        except (OSError, PermissionError) as e:
            print(f"Warning: Could not read {template_path}: {e}")

    raise FileNotFoundError(
        f"Prompt '{name}' not found in:\n"
        f"  - Project: {project_dir / 'prompts' if project_dir else 'N/A'}\n"
        f"  - Templates: {TEMPLATES_DIR}"
    )

def get_initializer_prompt(project_dir: Path | None = None) -> str:
    """Load the initializer prompt (project-specific if available)."""
    return load_prompt("initializer_prompt", project_dir)

def get_coding_prompt(project_dir: Path | None = None) -> str:
    """Load the coding agent prompt (project-specific if available)."""
    return load_prompt("coding_prompt", project_dir)

def get_coding_prompt_yolo(project_dir: Path | None = None) -> str:
    """Load the YOLO mode coding agent prompt (project-specific if available)."""
    return load_prompt("coding_prompt_yolo", project_dir)

def get_app_spec(project_dir: Path) -> str:
    """
    Load the app spec from the project.

    Checks in order:
    1. Project prompts directory: {project_dir}/prompts/app_spec.txt
    2. Project root (legacy): {project_dir}/app_spec.txt

    Args:
        project_dir: The project directory

    Returns:
        The app spec content

    Raises:
        FileNotFoundError: If no app_spec.txt found
    """
    # Try project prompts directory first
    project_prompts = get_project_prompts_dir(project_dir)
    spec_path = project_prompts / "app_spec.txt"
    if spec_path.exists():
        try:
            return spec_path.read_text(encoding="utf-8")
        except (OSError, PermissionError) as e:
            raise FileNotFoundError(f"Could not read {spec_path}: {e}") from e

    # Fallback to legacy location in project root
    legacy_spec = project_dir / "app_spec.txt"
    if legacy_spec.exists():
        try:
            return legacy_spec.read_text(encoding="utf-8")
        except (OSError, PermissionError) as e:
            raise FileNotFoundError(f"Could not read {legacy_spec}: {e}") from e

    raise FileNotFoundError(f"No app_spec.txt found for project: {project_dir}")

def scaffold_project_prompts(project_dir: Path) -> Path:
    """
    Create the project prompts directory and copy base templates.

    This sets up a new project with template files that can be customized.

    Args:
        project_dir: The absolute path to the project directory

    Returns:
        The path to the project prompts directory
    """
    project_prompts = get_project_prompts_dir(project_dir)
    project_prompts.mkdir(parents=True, exist_ok=True)

    # Define template mappings: (source_template, destination_name)
    templates = [
        ("app_spec.template.txt", "app_spec.txt"),
        ("coding_prompt.template.md", "coding_prompt.md"),
        ("coding_prompt_yolo.template.md", "coding_prompt_yolo.md"),
        ("initializer_prompt.template.md", "initializer_prompt.md"),
    ]

    copied_files = []
    for template_name, dest_name in templates:
        template_path = TEMPLATES_DIR / template_name
        dest_path = project_prompts / dest_name

        # Only copy if template exists and destination doesn't
        if template_path.exists() and not dest_path.exists():
            try:
                shutil.copy(template_path, dest_path)
                copied_files.append(dest_name)
            except (OSError, PermissionError) as e:
                print(f"  Warning: Could not copy {dest_name}: {e}")

    if copied_files:
        print(f"  Created prompt files: {', '.join(copied_files)}")

    return project_prompts

def has_project_prompts(project_dir: Path) -> bool:
    """
    Check if a project has valid prompts set up.

    A project has valid prompts if:
    1. The prompts directory exists, AND
    2. app_spec.txt exists within it, AND
    3. app_spec.txt contains the <project_specification> tag

    Args:
        project_dir: The project directory to check

    Returns:
        True if valid project prompts exist, False otherwise
    """
    project_prompts = get_project_prompts_dir(project_dir)
    app_spec = project_prompts / "app_spec.txt"

    if not app_spec.exists():
        # Also check legacy location in project root
        legacy_spec = project_dir / "app_spec.txt"
        if legacy_spec.exists():
            try:
                content = legacy_spec.read_text(encoding="utf-8")
                return "<project_specification>" in content
            except (OSError, PermissionError):
                return False
        return False

    # Check for valid spec content
    try:
        content = app_spec.read_text(encoding="utf-8")
        return "<project_specification>" in content
    except (OSError, PermissionError):
        return False

def copy_spec_to_project(project_dir: Path) -> None:
    """
    Copy the app spec file into the project root directory for the agent to read.

    This maintains backwards compatibility - the agent expects app_spec.txt
    in the project root directory.

    The spec is sourced from: {project_dir}/prompts/app_spec.txt

    Args:
        project_dir: The project directory
    """
    spec_dest = project_dir / "app_spec.txt"

    # Don't overwrite if already exists
    if spec_dest.exists():
        return

    # Copy from project prompts directory
    project_prompts = get_project_prompts_dir(project_dir)
    project_spec = project_prompts / "app_spec.txt"
    if project_spec.exists():
        try:
            shutil.copy(project_spec, spec_dest)
            print("Copied app_spec.txt to project directory")
            return
        except (OSError, PermissionError) as e:
            print(f"Warning: Could not copy app_spec.txt: {e}")
            return

    print("Warning: No app_spec.txt found to copy to project directory")

# =============================================================================
# Context Injection
# =============================================================================

def get_infrastructure_context(
    project_name: str,
    project_dir: Path,
    include_credentials: bool = True,
) -> str:
    """


Perfect! Now I have a comprehensive understanding of the codebase. Let me create a detailed analysis report of the session recovery/checkpointing architecture.

## Session Recovery/Checkpointing Analysis for Autocoder 2.0

Based on my thorough exploration of the Autocoder 2.0 codebase, here's a comprehensive analysis of the current session state management and recommendations for implementing session recovery/checkpointing:

---

### 1. CURRENT SESSION STATE ARCHITECTURE

#### A. Agent Session Lifecycle (`agent.py`)
The agent operates in a fresh context for each iteration:
- **No persistent message history**: Each iteration creates a new `ClaudeSDKClient` from scratch
- **Feature-driven iteration**: Uses `is_first_run` flag to determine if initializer or coding agent should run
- **Detection mechanism**: `has_features(project_dir)` checks if `features.db` has records to distinguish between fresh starts and continuations
- **Auto-continue pattern**: After each session completes, auto-continues after 3-second delay if status == "continue"

**Key limitation**: While feature state persists in the database, the Claude agent context is completely discarded between sessions. There's no memory of what work was done, what approaches were tried, or what blockers were encountered.

#### B. Process Lifecycle Management (`process_manager.py`)
**Lock file mechanism** (`.agent.lock`):
- Stores PID of running agent process
- Prevents multiple agents from running on same project
- Checked on startup; stale locks cleaned up
- Lock removed on process termination

**State tracking**:
```python
_status: Literal["stopped", "running", "paused", "crashed"]
started_at: datetime
yolo_mode: bool
pid: int
```

**Process tree killing**: Handles cleanup of agent + MCP servers + child processes on stop

**Health checking**: Can detect crashed processes and update status accordingly

#### C. Feature State Persistence (`api/database.py` + `mcp_server/feature_mcp.py`)

**Feature database schema** (SQLite):
```python
id: Integer (primary key)
priority: Integer (index)
category: String
name: String
description: Text
steps: JSON (array of strings)
passes: Boolean (index)  # Marks completion
in_progress: Boolean (index)  # Marks active work
```

**MCP tools for state mutation**:
- `feature_mark_passing(feature_id)` - Mark complete
- `feature_mark_in_progress(feature_id)` - Lock feature for work
- `feature_clear_in_progress(feature_id)` - Unlock on failure
- `feature_skip(feature_id)` - Move to end of queue
- `feature_get_next()` - Get highest-priority pending feature
- `feature_get_stats()` - Get passing/in_progress/total counts

**Progress caching** (`progress.py`):
- Stores passing feature IDs in `.progress_cache` JSON file
- Used to detect new passing tests and send webhooks
- Backup: Database queries directly count features with `passes=1`

#### D. File Locking for Parallel Work (`file_lock.py`)

**In-memory lock manager**:
- Async-safe, per-file locks with expiration
- Supports timeouts (default 1 hour)
- Used to coordinate parallel agents
- Lock info: `file_path`, `feature_id`, `agent_id`, `locked_at`, `expires_at`
- Can be force-released for recovery

**Limitation**: Locks are in-memory only, lost on process restart

#### E. Migration/Backward Compatibility (`api/migration.py`)

**JSON to SQLite migration**:
- Detects legacy `feature_list.json`
- Imports to database if DB is empty
- Renames JSON to backup with timestamp
- Enables smooth upgrades

**Export functionality**: Can export features back to JSON for debugging

---

### 2. WHAT STATE EXISTS DURING AN AGENT SESSION

#### Session State (Ephemeral - Lost on Exit)
```
Claude SDK Client Context:
├── Message history (lost after session)
├── Tool execution results (lost after session)
├── Current working context (lost after session)
└── Intermediate file state (persisted via Write/Edit tools)
```

#### Persistent State (Disk-based)
```
Project Directory/
├── features.db (SQLite)
│   ├── Feature table with passes/in_progress flags
│   ├── Auto-incremented IDs for each feature
│   └── Priority ordering
├── .agent.lock (PID file)
├── .progress_cache (JSON - passing feature IDs)
├── Generated code files (created by agent)
├── .claude_settings.json (security config per session)
└── [project files created/modified by agent]
```

---

### 3. KEY CHECKPOINT TRIGGERS & RECOVERY POINTS

#### Natural Checkpoint Opportunities
1. **After tool use completes** (Write/Edit/Bash)
   - Tool result is received
   - State before tool can be saved
   - Recovery can resume from tool result

2. **After feature marked passing**
   - `feature_mark_passing(feature_id)` called
   - Database transaction committed
   - Next session picks up at `feature_get_next()`

3. **After feature marked in_progress**
   - `feature_mark_in_progress(feature_id)` called
   - Prevents duplicate work if session crashes
   - Recovery can detect stuck features via stale in_progress flags

4. **On session start**
   - Check for interrupted features (in_progress=1 but not by current session)
   - Could resume work or reset stale flags

5. **On session end**
   - Summary of work completed
   - Reset any abandoned in_progress features
   - Create checkpoint for next iteration

---

### 4. RECOMMENDATIONS FOR RECOVERY/CHECKPOINTING SYSTEM

#### A. Conversation History Checkpoint System

**Create a session journal** (`project_dir/.session_journal/`):
```
.session_journal/
├── session_001.json          # Structured log of first session
├── session_002.json          # Structured log of second session
└── current_session.json      # Active session being written to

Schema per session:
{
  "session_id": "uuid",
  "started_at": "ISO timestamp",
  "ended_at": "ISO timestamp",
  "iteration": 1,
  "is_initializer": false,
  "feature_id": 42,
  "initial_stats": { "passing": 10, "in_progress": 2, "total": 50 },
  "final_stats": { "passing": 12, "in_progress": 0, "total": 50 },
  "messages": [
    {
      "role": "user",
      "content": "...",
      "timestamp": "ISO"
    },
    {
      "role": "assistant",
      "content": "...",
      "tools_used": ["feature_mark_passing"],
      "timestamp": "ISO"
    }
  ],
  "blockers": ["Dependency on feature #5", "..."],
  "decisions": ["Skipped feature #10 due to unclear requirements"],
  "errors": ["Tool X failed with error Y"],
  "status": "success|incomplete|error",
  "checkpoints": [
    {
      "timestamp": "ISO",
      "trigger": "after_tool_use|after_feature_complete",
      "tool_name": "Write",
      "state_summary": "..."
    }
  ]
}
```

**Benefits**:
- Can construct conversation summary for next session
- Helps agent learn from previous decisions
- Enables "session replay" for debugging
- Tracks what didn't work to avoid retry

#### B. Feature Work State Tracking

**Extend Feature model** with recovery metadata:
```python
# Add columns to features table:
last_worked_on: DateTime        # When feature was last attempted
work_session_id: String         # Which session last worked on it
attempt_count: Integer          # How many times attempted
last_error: Text                # Last error encountered
blocked_by_features: JSON       # List of feature IDs blocking this
recovery_notes: Text            # Notes from agent about state
```

**Benefits**:
- Detect truly stuck features
- Avoid infinite retry loops
- Track which sessions touched which features
- Support intelligent recovery decisions

#### C. Tool Execution Checkpoint

**Track tool results** in session journal:
```python
{
  "tool_execution": {
    "sequence": 1,
    "tool_name": "Write",
    "file_path": "src/component.tsx",
    "input": {...},
    "result": "Success" | "Error: ...",
    "state_before": {
      "feature_id": 42,
      "passing_count": 10,
      "modified_files": ["src/component.tsx"]
    },
    "state_after": {
      "feature_id": 42,
      "passing_count": 10,
      "modified_files": ["src/component.tsx"]
    }
  }
}
```

**Benefits**:
- Resume from exact point of interruption
- Provides context for tool results
- Enables idempotent retries

#### D. Stale Work Detection & Recovery

**In `process_manager` or new recovery module**:
```python
def detect_stale_work(project_dir, timeout_minutes=60):
    """Find features stuck in in_progress state."""
    session = get_session()

    stuck = session.query(Feature).filter(
        Feature.in_progress == True,
        Feature.updated_at < datetime.now() - timedelta(minutes=timeout_minutes)
    ).all()

    return stuck

def recover_stale_work(project_dir):
    """
    On agent startup:
    1. Detect features in in_progress state for >60 minutes
    2. Load last session journal for that feature
    3. Check for blockers/errors
    4. Either reset or add to skip queue
    """
    stuck_features = detect_stale_work(project_dir)
    for feature in stuck_features:
        journal = load_session_journal(project_dir, feature.work_session_id)
        if journal.get("status") == "error":
            # Previous session errored - try again or skip
            feature.in_progress = False
            # Could implement escalation: add to queue with recovery_notes
        elif journal.get("blockers"):
            # Previous session had blockers - skip for now
            feature_skip(feature.id)
```

**Benefits**:
- Automatic recovery from crashes
- Prevents zombie work assignments
- Smart handling of blockers

#### E. Session Context Injection for Recovery

**Modify `prompts.py`** to include recovery context:
```python
def get_recovery_context(project_dir: Path) -> str:
    """Build context about previous work for recovery."""

    # Load last session journal
    last_session = load_latest_session_journal(project_dir)

    if not last_session:
        return ""

    context = f"""
## Recovery Context from Previous Session

**Previous Session Status**: {last_session.get('status')}
**Features Completed**: {last_session['final_stats']['passing']}
**Features Attempted**: {last_session.get('feature_id')}

**Previous Blockers**:
{json.dumps(last_session.get('blockers', []), indent=2)}

**Previous Errors**:
{json.dumps(last_session.get('errors', []), indent=2)}

**Decisions Made**:
{json.dumps(last_session.get('decisions', []), indent=2)}

**Session Duration**: {(last_session['ended_at'] - last_session['started_at']).total_seconds()} seconds
"""

    return context

# Inject into prompt:
prompt = get_coding_prompt_with_context(project_dir)
if is_recovery_session:
    recovery_context = get_recovery_context(project_dir)
    prompt = recovery_context + "\n\n" + prompt
```

**Benefits**:
- Agent learns from previous mistakes
- Avoids trying same failed approaches
- Can handle blockers more intelligently
- Provides continuity to human observers

---

### 5. IMPLEMENTATION PRIORITY ROADMAP

#### Phase 1: Foundation (Session Journal)
- Create `.session_journal/` with JSON logging
- Log all messages and tool uses
- Track feature stats at start/end
- **Effort**: 2-3 days | **Impact**: High (enables all other phases)

#### Phase 2: Feature Work Metadata
- Add `last_worked_on`, `work_session_id`, `attempt_count`, `last_error` to Feature model
- Track updates on each mark_in_progress/mark_passing call
- Add stale work detection
- **Effort**: 2-3 days | **Impact**: High (enables recovery)

#### Phase 3: Recovery Context
- Inject last session summary into next agent prompt
- Analyze blockers and errors automatically
- Skip features intelligently based on history
- **Effort**: 2-3 days | **Impact**: Medium (improves convergence)

#### Phase 4: Intelligent Resumption
- Detect stuck features on startup
- Clear stale in_progress flags with timeout
- Surface recovery notes to agent
- **Effort**: 2-3 days | **Impact**: Medium (operational stability)

#### Phase 5: Advanced Features (Optional)
- Message history reconstruction from journal
- Tool result caching for idempotent execution
- Automated root cause analysis of failures
- **Effort**: 3-4 days | **Impact**: Low (nice to have)

---

### 6. EXISTING PATTERNS TO FOLLOW

✓ **Database migrations**: Use SQLAlchemy migrations for new columns
✓ **File organization**: Follow `.project_dir` structure convention
✓ **JSON serialization**: Use datetime.isoformat() for timestamps
✓ **Error handling**: Log to session journal; don't crash on missing data
✓ **Async safety**: Use asyncio.Lock if accessed from multiple tasks
✓ **Recovery**: Follow `process_manager` pattern of graceful cleanup

---

### 7. KEY FILES TO MODIFY

1. **`api/database.py`** - Add feature metadata columns
2. **`mcp_server/feature_mcp.py`** - Update to set metadata on mutations
3. **`agent.py`** - Create session journal on startup/shutdown
4. **`prompts.py`** - Add recovery context injection
5. **`progress.py`** - Add session journal logging
6. **NEW**: `session_recovery.py` - Recovery module (stale detection, context building)

---

### 8. CRITICAL INSIGHTS

- **No message history exists**: Each session is stateless regarding conversation - this is the biggest gap
- **Feature state is solid**: Database design supports tracking work well
- **Process management works**: Lock files and PID tracking prevent conflicts
- **Recovery untested**: Current system would crash and lose context on agent failure
- **Blockers are invisible**: No mechanism to track why features are skipped
- **Decisions are lost**: Agent discoveries about requirements/design aren't recorded

The system is well-architected for **feature state tracking** but lacks **session/decision tracking** for recovery and learning. The three essential additions are: (1) session journals, (2) feature work metadata, and (3) recovery context injection.