     1→"""
     2→Spec Creation Chat Session
     3→==========================
     4→
     5→Manages interactive spec creation conversation with Claude.
     6→Uses the create-spec.md skill to guide users through app spec creation.
     7→"""
     8→
     9→import json
    10→import logging
    11→import shutil
    12→import threading
    13→from datetime import datetime
    14→from pathlib import Path
    15→from typing import AsyncGenerator, Optional
    16→
    17→from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
    18→
    19→from ..schemas import ImageAttachment
    20→
    21→logger = logging.getLogger(__name__)
    22→
    23→
    24→async def _make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
    25→    """
    26→    Create an async generator that yields a properly formatted multimodal message.
    27→
    28→    The Claude Agent SDK's query() method accepts either:
    29→    - A string (simple text)
    30→    - An AsyncIterable[dict] (for custom message formats)
    31→
    32→    This function wraps content blocks in the expected message format.
    33→    """
    34→    yield {
    35→        "type": "user",
    36→        "message": {"role": "user", "content": content_blocks},
    37→        "parent_tool_use_id": None,
    38→        "session_id": "default",
    39→    }
    40→
    41→# Root directory of the project
    42→ROOT_DIR = Path(__file__).parent.parent.parent
    43→
    44→
    45→class SpecChatSession:
    46→    """
    47→    Manages a spec creation conversation for one project.
    48→
    49→    For NEW projects, uses create-spec skill to guide users through:
    50→    - Phase 1: Project Overview (name, description, audience)
    51→    - Phase 2: Involvement Level (Quick vs Detailed mode)
    52→    - Phase 3: Technology Preferences
    53→    - Phase 4: Features (main exploration phase)
    54→    - Phase 5: Technical Details (derived or discussed)
    55→    - Phase 6-7: Success Criteria & Approval
    56→
    57→    For IMPORTED projects, uses import-spec skill to:
    58→    - Present codebase analysis
    59→    - Document existing features
    60→    - Identify gaps and plan new features
    61→    """
    62→
    63→    def __init__(
    64→        self,
    65→        project_name: str,
    66→        project_dir: Path,
    67→        is_import: bool = False,
    68→        codebase_analysis: Optional[str] = None,
    69→    ):
    70→        """
    71→        Initialize the session.
    72→
    73→        Args:
    74→            project_name: Name of the project being created
    75→            project_dir: Absolute path to the project directory
    76→            is_import: If True, use import-spec skill instead of create-spec
    77→            codebase_analysis: Formatted codebase analysis for import mode
    78→        """
    79→        self.project_name = project_name
    80→        self.project_dir = project_dir
    81→        self.is_import = is_import
    82→        self.codebase_analysis = codebase_analysis
    83→        self.client: Optional[ClaudeSDKClient] = None
    84→        self.messages: list[dict] = []
    85→        self.complete: bool = False
    86→        self.created_at = datetime.now()
    87→        self._conversation_id: Optional[str] = None
    88→        self._client_entered: bool = False  # Track if context manager is active
    89→
    90→    async def close(self) -> None:
    91→        """Clean up resources and close the Claude client."""
    92→        if self.client and self._client_entered:
    93→            try:
    94→                await self.client.__aexit__(None, None, None)
    95→            except Exception as e:
    96→                logger.warning(f"Error closing Claude client: {e}")
    97→            finally:
    98→                self._client_entered = False
    99→                self.client = None
   100→
   101→    async def start(self) -> AsyncGenerator[dict, None]:
   102→        """
   103→        Initialize session and get initial greeting from Claude.
   104→
   105→        Yields message chunks as they stream in.
   106→        """
   107→        # Load the appropriate skill based on import mode
   108→        if self.is_import:
   109→            skill_path = ROOT_DIR / ".claude" / "commands" / "import-spec.md"
   110→            skill_type = "import"
   111→        else:
   112→            skill_path = ROOT_DIR / ".claude" / "commands" / "create-spec.md"
   113→            skill_type = "create"
   114→
   115→        if not skill_path.exists():
   116→            yield {
   117→                "type": "error",
   118→                "content": f"Spec {skill_type} skill not found at {skill_path}"
   119→            }
   120→            return
   121→
   122→        try:
   123→            skill_content = skill_path.read_text(encoding="utf-8")
   124→        except UnicodeDecodeError:
   125→            skill_content = skill_path.read_text(encoding="utf-8", errors="replace")
   126→
   127→        # Ensure project directory exists (like CLI does in start.py)
   128→        self.project_dir.mkdir(parents=True, exist_ok=True)
   129→
   130→        # Delete app_spec.txt so Claude can create it fresh
   131→        # The SDK requires reading existing files before writing, but app_spec.txt is created new
   132→        # Note: We keep initializer_prompt.md so Claude can read and update the template
   133→        prompts_dir = self.project_dir / "prompts"
   134→        app_spec_path = prompts_dir / "app_spec.txt"
   135→        if app_spec_path.exists():
   136→            app_spec_path.unlink()
   137→            logger.info("Deleted scaffolded app_spec.txt for fresh spec creation")
   138→
   139→        # Create security settings file (like client.py does)
   140→        # This grants permissions for file operations in the project directory
   141→        security_settings = {
   142→            "sandbox": {"enabled": False},  # Disable sandbox for spec creation
   143→            "permissions": {
   144→                "defaultMode": "acceptEdits",
   145→                "allow": [
   146→                    "Read(./**)",
   147→                    "Write(./**)",
   148→                    "Edit(./**)",
   149→                    "Glob(./**)",
   150→                ],
   151→            },
   152→        }
   153→        settings_file = self.project_dir / ".claude_settings.json"
   154→        with open(settings_file, "w") as f:
   155→            json.dump(security_settings, f, indent=2)
   156→
   157→        # Replace $ARGUMENTS with absolute project path (like CLI does in start.py:184)
   158→        # Using absolute path avoids confusion when project folder name differs from app name
   159→        project_path = str(self.project_dir.resolve())
   160→        system_prompt = skill_content.replace("$ARGUMENTS", project_path)
   161→
   162→        # For import mode, replace $CODEBASE_ANALYSIS with the analysis content
   163→        if self.is_import and self.codebase_analysis:
   164→            system_prompt = system_prompt.replace("$CODEBASE_ANALYSIS", self.codebase_analysis)
   165→        elif self.is_import:
   166→            # No analysis provided - use placeholder
   167→            system_prompt = system_prompt.replace(
   168→                "$CODEBASE_ANALYSIS",
   169→                "*No automated analysis available. Please describe your project.*"
   170→            )
   171→
   172→        # Create Claude SDK client with limited tools for spec creation
   173→        # Use Opus for best quality spec generation
   174→        # Use system CLI to avoid bundled Bun runtime crash (exit code 3) on Windows
   175→        system_cli = shutil.which("claude")
   176→        try:
   177→            self.client = ClaudeSDKClient(
   178→                options=ClaudeAgentOptions(
   179→                    model="claude-opus-4-5-20251101",
   180→                    cli_path=system_cli,
   181→                    system_prompt=system_prompt,
   182→                    allowed_tools=[
   183→                        "Read",
   184→                        "Write",
   185→                        "Edit",
   186→                        "Glob",
   187→                    ],
   188→                    permission_mode="acceptEdits",  # Auto-approve file writes for spec creation
   189→                    max_turns=100,
   190→                    cwd=str(self.project_dir.resolve()),
   191→                    settings=str(settings_file.resolve()),
   192→                )
   193→            )
   194→            # Enter the async context and track it
   195→            await self.client.__aenter__()
   196→            self._client_entered = True
   197→        except Exception as e:
   198→            logger.exception("Failed to create Claude client")
   199→            yield {
   200→                "type": "error",
   201→                "content": f"Failed to initialize Claude: {str(e)}"
   202→            }
   203→            return
   204→
   205→        # Start the conversation - Claude will send the Phase 1 greeting
   206→        try:
   207→            async for chunk in self._query_claude("Begin the spec creation process."):
   208→                yield chunk
   209→            # Signal that the response is complete (for UI to hide loading indicator)
   210→            yield {"type": "response_done"}
   211→        except Exception as e:
   212→            logger.exception("Failed to start spec chat")
   213→            yield {
   214→                "type": "error",
   215→                "content": f"Failed to start conversation: {str(e)}"
   216→            }
   217→
   218→    async def send_message(
   219→        self,
   220→        user_message: str,
   221→        attachments: list[ImageAttachment] | None = None
   222→    ) -> AsyncGenerator[dict, None]:
   223→        """
   224→        Send user message and stream Claude's response.
   225→
   226→        Args:
   227→            user_message: The user's response
   228→            attachments: Optional list of image attachments
   229→
   230→        Yields:
   231→            Message chunks of various types:
   232→            - {"type": "text", "content": str}
   233→            - {"type": "question", "questions": list}
   234→            - {"type": "spec_complete", "path": str}
   235→            - {"type": "error", "content": str}
   236→        """
   237→        if not self.client:
   238→            yield {
   239→                "type": "error",
   240→                "content": "Session not initialized. Call start() first."
   241→            }
   242→            return
   243→
   244→        # Store the user message
   245→        self.messages.append({
   246→            "role": "user",
   247→            "content": user_message,
   248→            "has_attachments": bool(attachments),
   249→            "timestamp": datetime.now().isoformat()
   250→        })
   251→
   252→        try:
   253→            async for chunk in self._query_claude(user_message, attachments):
   254→                yield chunk
   255→            # Signal that the response is complete (for UI to hide loading indicator)
   256→            yield {"type": "response_done"}
   257→        except Exception as e:
   258→            logger.exception("Error during Claude query")
   259→            yield {
   260→                "type": "error",
   261→                "content": f"Error: {str(e)}"
   262→            }
   263→
   264→    async def _query_claude(
   265→        self,
   266→        message: str,
   267→        attachments: list[ImageAttachment] | None = None
   268→    ) -> AsyncGenerator[dict, None]:
   269→        """
   270→        Internal method to query Claude and stream responses.
   271→
   272→        Handles tool calls (Write) and text responses.
   273→        Supports multimodal content with image attachments.
   274→
   275→        IMPORTANT: Spec creation requires BOTH files to be written:
   276→        1. app_spec.txt - the main specification
   277→        2. initializer_prompt.md - tells the agent how many features to create
   278→
   279→        We only signal spec_complete when BOTH files are verified on disk.
   280→        """
   281→        if not self.client:
   282→            return
   283→
   284→        # Build the message content
   285→        if attachments and len(attachments) > 0:
   286→            # Multimodal message: build content blocks array
   287→            content_blocks = []
   288→
   289→            # Add text block if there's text
   290→            if message:
   291→                content_blocks.append({"type": "text", "text": message})
   292→
   293→            # Add image blocks
   294→            for att in attachments:
   295→                content_blocks.append({
   296→                    "type": "image",
   297→                    "source": {
   298→                        "type": "base64",
   299→                        "media_type": att.mimeType,
   300→                        "data": att.base64Data,
   301→                    }
   302→                })
   303→
   304→            # Send multimodal content to Claude using async generator format
   305→            # The SDK's query() accepts AsyncIterable[dict] for custom message formats
   306→            await self.client.query(_make_multimodal_message(content_blocks))
   307→            logger.info(f"Sent multimodal message with {len(attachments)} image(s)")
   308→        else:
   309→            # Text-only message: use string format
   310→            await self.client.query(message)
   311→
   312→        current_text = ""
   313→
   314→        # Track pending writes for BOTH required files
   315→        pending_writes = {
   316→            "app_spec": None,      # {"tool_id": ..., "path": ...}
   317→            "initializer": None,   # {"tool_id": ..., "path": ...}
   318→        }
   319→
   320→        # Track which files have been successfully written
   321→        files_written = {
   322→            "app_spec": False,
   323→            "initializer": False,
   324→        }
   325→
   326→        # Store paths for the completion message
   327→        spec_path = None
   328→
   329→        # Stream the response using receive_response
   330→        async for msg in self.client.receive_response():
   331→            msg_type = type(msg).__name__
   332→
   333→            if msg_type == "AssistantMessage" and hasattr(msg, "content"):
   334→                # Process content blocks in the assistant message
   335→                for block in msg.content:
   336→                    block_type = type(block).__name__
   337→
   338→                    if block_type == "TextBlock" and hasattr(block, "text"):
   339→                        # Accumulate text and yield it
   340→                        text = block.text
   341→                        if text:
   342→                            current_text += text
   343→                            yield {"type": "text", "content": text}
   344→
   345→                            # Store in message history
   346→                            self.messages.append({
   347→                                "role": "assistant",
   348→                                "content": text,
   349→                                "timestamp": datetime.now().isoformat()
   350→                            })
   351→
   352→                    elif block_type == "ToolUseBlock" and hasattr(block, "name"):
   353→                        tool_name = block.name
   354→                        tool_input = getattr(block, "input", {})
   355→                        tool_id = getattr(block, "id", "")
   356→
   357→                        if tool_name in ("Write", "Edit"):
   358→                            # File being written or edited - track for verification
   359→                            file_path = tool_input.get("file_path", "")
   360→
   361→                            # Track app_spec.txt
   362→                            if "app_spec.txt" in str(file_path):
   363→                                pending_writes["app_spec"] = {
   364→                                    "tool_id": tool_id,
   365→                                    "path": file_path
   366→                                }
   367→                                logger.info(f"{tool_name} tool called for app_spec.txt: {file_path}")
   368→
   369→                            # Track initializer_prompt.md
   370→                            elif "initializer_prompt.md" in str(file_path):
   371→                                pending_writes["initializer"] = {
   372→                                    "tool_id": tool_id,
   373→                                    "path": file_path
   374→                                }
   375→                                logger.info(f"{tool_name} tool called for initializer_prompt.md: {file_path}")
   376→
   377→            elif msg_type == "UserMessage" and hasattr(msg, "content"):
   378→                # Tool results - check for write confirmations and errors
   379→                for block in msg.content:
   380→                    block_type = type(block).__name__
   381→                    if block_type == "ToolResultBlock":
   382→                        is_error = getattr(block, "is_error", False)
   383→                        tool_use_id = getattr(block, "tool_use_id", "")
   384→
   385→                        if is_error:
   386→                            content = getattr(block, "content", "Unknown error")
   387→                            logger.warning(f"Tool error: {content}")
   388→                            # Clear any pending writes that failed
   389→                            for key in pending_writes:
   390→                                if pending_writes[key] and tool_use_id == pending_writes[key].get("tool_id"):
   391→                                    logger.error(f"{key} write failed: {content}")
   392→                                    pending_writes[key] = None
   393→                        else:
   394→                            # Tool succeeded - check which file was written
   395→
   396→                            # Check app_spec.txt
   397→                            if pending_writes["app_spec"] and tool_use_id == pending_writes["app_spec"].get("tool_id"):
   398→                                file_path = pending_writes["app_spec"]["path"]
   399→                                full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
   400→                                if full_path.exists():
   401→                                    logger.info(f"app_spec.txt verified at: {full_path}")
   402→                                    files_written["app_spec"] = True
   403→                                    spec_path = file_path
   404→
   405→                                    # Notify about file write (but NOT completion yet)
   406→                                    yield {
   407→                                        "type": "file_written",
   408→                                        "path": str(file_path)
   409→                                    }
   410→                                else:
   411→                                    logger.error(f"app_spec.txt not found after write: {full_path}")
   412→                                pending_writes["app_spec"] = None
   413→
   414→                            # Check initializer_prompt.md
   415→                            if pending_writes["initializer"] and tool_use_id == pending_writes["initializer"].get("tool_id"):
   416→                                file_path = pending_writes["initializer"]["path"]
   417→                                full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
   418→                                if full_path.exists():
   419→                                    logger.info(f"initializer_prompt.md verified at: {full_path}")
   420→                                    files_written["initializer"] = True
   421→
   422→                                    # Notify about file write
   423→                                    yield {
   424→                                        "type": "file_written",
   425→                                        "path": str(file_path)
   426→                                    }
   427→                                else:
   428→                                    logger.error(f"initializer_prompt.md not found after write: {full_path}")
   429→                                pending_writes["initializer"] = None
   430→
   431→                            # Check if BOTH files are now written - only then signal completion
   432→                            if files_written["app_spec"] and files_written["initializer"]:
   433→                                logger.info("Both app_spec.txt and initializer_prompt.md verified - signaling completion")
   434→                                self.complete = True
   435→                                yield {
   436→                                    "type": "spec_complete",
   437→                                    "path": str(spec_path)
   438→                                }
   439→
   440→    def is_complete(self) -> bool:
   441→        """Check if spec creation is complete."""
   442→        return self.complete
   443→
   444→    def get_messages(self) -> list[dict]:
   445→        """Get all messages in the conversation."""
   446→        return self.messages.copy()
   447→
   448→
   449→# Session registry with thread safety
   450→_sessions: dict[str, SpecChatSession] = {}
   451→_sessions_lock = threading.Lock()
   452→
   453→
   454→def get_session(project_name: str) -> Optional[SpecChatSession]:
   455→    """Get an existing session for a project."""
   456→    with _sessions_lock:
   457→        return _sessions.get(project_name)
   458→
   459→
   460→async def create_session(
   461→    project_name: str,
   462→    project_dir: Path,
   463→    is_import: bool = False,
   464→    codebase_analysis: Optional[str] = None,
   465→) -> SpecChatSession:
   466→    """Create a new session for a project, closing any existing one.
   467→
   468→    Args:
   469→        project_name: Name of the project
   470→        project_dir: Absolute path to the project directory
   471→        is_import: If True, use import-spec skill for existing codebases
   472→        codebase_analysis: Formatted analysis content for import mode
   473→    """
   474→    old_session: Optional[SpecChatSession] = None
   475→
   476→    with _sessions_lock:
   477→        # Get existing session to close later (outside the lock)
   478→        old_session = _sessions.pop(project_name, None)
   479→        session = SpecChatSession(
   480→            project_name,
   481→            project_dir,
   482→            is_import=is_import,
   483→            codebase_analysis=codebase_analysis,
   484→        )
   485→        _sessions[project_name] = session
   486→
   487→    # Close old session outside the lock to avoid blocking
   488→    if old_session:
   489→        try:
   490→            await old_session.close()
   491→        except Exception as e:
   492→            logger.warning(f"Error closing old session for {project_name}: {e}")
   493→
   494→    return session
   495→
   496→
   497→async def remove_session(project_name: str) -> None:
   498→    """Remove and close a session."""
   499→    session: Optional[SpecChatSession] = None
   500→
   501→    with _sessions_lock:
   502→        session = _sessions.pop(project_name, None)
   503→
   504→    # Close session outside the lock
   505→    if session:
   506→        try:
   507→            await session.close()
   508→        except Exception as e:
   509→            logger.warning(f"Error closing session for {project_name}: {e}")
   510→
   511→
   512→def list_sessions() -> list[str]:
   513→    """List all active session project names."""
   514→    with _sessions_lock:
   515→        return list(_sessions.keys())
   516→
   517→
   518→async def cleanup_all_sessions() -> None:
   519→    """Close all active sessions. Called on server shutdown."""
   520→    sessions_to_close: list[SpecChatSession] = []
   521→
   522→    with _sessions_lock:
   523→        sessions_to_close = list(_sessions.values())
   524→        _sessions.clear()
   525→
   526→    for session in sessions_to_close:
   527→        try:
   528→            await session.close()
   529→        except Exception as e:
   530→            logger.warning(f"Error closing session {session.project_name}: {e}")
   531→

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

"""
Spec Creation Chat Session
==========================

Manages interactive spec creation conversation with Claude.
Uses the create-spec.md skill to guide users through app spec creation.
"""

import json
import logging
import shutil
import threading
from datetime import datetime
from pathlib import Path
from typing import AsyncGenerator, Optional

from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient

from ..schemas import ImageAttachment

logger = logging.getLogger(__name__)

async def _make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
    """
    Create an async generator that yields a properly formatted multimodal message.

    The Claude Agent SDK's query() method accepts either:
    - A string (simple text)
    - An AsyncIterable[dict] (for custom message formats)

    This function wraps content blocks in the expected message format.
    """
    yield {
        "type": "user",
        "message": {"role": "user", "content": content_blocks},
        "parent_tool_use_id": None,
        "session_id": "default",
    }

# Root directory of the project
ROOT_DIR = Path(__file__).parent.parent.parent

class SpecChatSession:
    """
    Manages a spec creation conversation for one project.

    For NEW projects, uses create-spec skill to guide users through:
    - Phase 1: Project Overview (name, description, audience)
    - Phase 2: Involvement Level (Quick vs Detailed mode)
    - Phase 3: Technology Preferences
    - Phase 4: Features (main exploration phase)
    - Phase 5: Technical Details (derived or discussed)
    - Phase 6-7: Success Criteria & Approval

    For IMPORTED projects, uses import-spec skill to:
    - Present codebase analysis
    - Document existing features
    - Identify gaps and plan new features
    """

    def __init__(
        self,
        project_name: str,
        project_dir: Path,
        is_import: bool = False,
        codebase_analysis: Optional[str] = None,
    ):
        """
        Initialize the session.

        Args:
            project_name: Name of the project being created
            project_dir: Absolute path to the project directory
            is_import: If True, use import-spec skill instead of create-spec
            codebase_analysis: Formatted codebase analysis for import mode
        """
        self.project_name = project_name
        self.project_dir = project_dir
        self.is_import = is_import
        self.codebase_analysis = codebase_analysis
        self.client: Optional[ClaudeSDKClient] = None
        self.messages: list[dict] = []
        self.complete: bool = False
        self.created_at = datetime.now()
        self._conversation_id: Optional[str] = None
        self._client_entered: bool = False  # Track if context manager is active

    async def close(self) -> None:
        """Clean up resources and close the Claude client."""
        if self.client and self._client_entered:
            try:
                await self.client.__aexit__(None, None, None)
            except Exception as e:
                logger.warning(f"Error closing Claude client: {e}")
            finally:
                self._client_entered = False
                self.client = None

    async def start(self) -> AsyncGenerator[dict, None]:
        """
        Initialize session and get initial greeting from Claude.

        Yields message chunks as they stream in.
        """
        # Load the appropriate skill based on import mode
        if self.is_import:
            skill_path = ROOT_DIR / ".claude" / "commands" / "import-spec.md"
            skill_type = "import"
        else:
            skill_path = ROOT_DIR / ".claude" / "commands" / "create-spec.md"
            skill_type = "create"

        if not skill_path.exists():
            yield {
                "type": "error",
                "content": f"Spec {skill_type} skill not found at {skill_path}"
            }
            return

        try:
            skill_content = skill_path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            skill_content = skill_path.read_text(encoding="utf-8", errors="replace")

        # Ensure project directory exists (like CLI does in start.py)
        self.project_dir.mkdir(parents=True, exist_ok=True)

        # Delete app_spec.txt so Claude can create it fresh
        # The SDK requires reading existing files before writing, but app_spec.txt is created new
        # Note: We keep initializer_prompt.md so Claude can read and update the template
        prompts_dir = self.project_dir / "prompts"
        app_spec_path = prompts_dir / "app_spec.txt"
        if app_spec_path.exists():
            app_spec_path.unlink()
            logger.info("Deleted scaffolded app_spec.txt for fresh spec creation")

        # Create security settings file (like client.py does)
        # This grants permissions for file operations in the project directory
        security_settings = {
            "sandbox": {"enabled": False},  # Disable sandbox for spec creation
            "permissions": {
                "defaultMode": "acceptEdits",
                "allow": [
                    "Read(./**)",
                    "Write(./**)",
                    "Edit(./**)",
                    "Glob(./**)",
                ],
            },
        }
        settings_file = self.project_dir / ".claude_settings.json"
        with open(settings_file, "w") as f:
            json.dump(security_settings, f, indent=2)

        # Replace $ARGUMENTS with absolute project path (like CLI does in start.py:184)
        # Using absolute path avoids confusion when project folder name differs from app name
        project_path = str(self.project_dir.resolve())
        system_prompt = skill_content.replace("$ARGUMENTS", project_path)

        # For import mode, replace $CODEBASE_ANALYSIS with the analysis content
        if self.is_import and self.codebase_analysis:
            system_prompt = system_prompt.replace("$CODEBASE_ANALYSIS", self.codebase_analysis)
        elif self.is_import:
            # No analysis provided - use placeholder
            system_prompt = system_prompt.replace(
                "$CODEBASE_ANALYSIS",
                "*No automated analysis available. Please describe your project.*"
            )

        # Create Claude SDK client with limited tools for spec creation
        # Use Opus for best quality spec generation
        # Use system CLI to avoid bundled Bun runtime crash (exit code 3) on Windows
        system_cli = shutil.which("claude")
        try:
            self.client = ClaudeSDKClient(
                options=ClaudeAgentOptions(
                    model="claude-opus-4-5-20251101",
                    cli_path=system_cli,
                    system_prompt=system_prompt,
                    allowed_tools=[
                        "Read",
                        "Write",
                        "Edit",
                        "Glob",
                    ],
                    permission_mode="acceptEdits",  # Auto-approve file writes for spec creation
                    max_turns=100,
                    cwd=str(self.project_dir.resolve()),
                    settings=str(settings_file.resolve()),
                )
            )
            # Enter the async context and track it
            await self.client.__aenter__()
            self._client_entered = True
        except Exception as e:
            logger.exception("Failed to create Claude client")
            yield {
                "type": "error",
                "content": f"Failed to initialize Claude: {str(e)}"
            }
            return

        # Start the conversation - Claude will send the Phase 1 greeting
        try:
            async for chunk in self._query_claude("Begin the spec creation process."):
                yield chunk
            # Signal that the response is complete (for UI to hide loading indicator)
            yield {"type": "response_done"}
        except Exception as e:
            logger.exception("Failed to start spec chat")
            yield {
                "type": "error",
                "content": f"Failed to start conversation: {str(e)}"
            }

    async def send_message(
        self,
        user_message: str,
        attachments: list[ImageAttachment] | None = None
    ) -> AsyncGenerator[dict, None]:
        """
        Send user message and stream Claude's response.

        Args:
            user_message: The user's response
            attachments: Optional list of image attachments

        Yields:
            Message chunks of various types:
            - {"type": "text", "content": str}
            - {"type": "question", "questions": list}
            - {"type": "spec_complete", "path": str}
            - {"type": "error", "content": str}
        """
        if not self.client:
            yield {
                "type": "error",
                "content": "Session not initialized. Call start() first."
            }
            return

        # Store the user message
        self.messages.append({
            "role": "user",
            "content": user_message,
            "has_attachments": bool(attachments),
            "timestamp": datetime.now().isoformat()
        })

        try:
            async for chunk in self._query_claude(user_message, attachments):
                yield chunk
            # Signal that the response is complete (for UI to hide loading indicator)
            yield {"type": "response_done"}
        except Exception as e:
            logger.exception("Error during Claude query")
            yield {
                "type": "error",
                "content": f"Error: {str(e)}"
            }

    async def _query_claude(
        self,
        message: str,
        attachments: list[ImageAttachment] | None = None
    ) -> AsyncGenerator[dict, None]:
        """
        Internal method to query Claude and stream responses.

        Handles tool calls (Write) and text responses.
        Supports multimodal content with image attachments.

        IMPORTANT: Spec creation requires BOTH files to be written:
        1. app_spec.txt - the main specification
        2. initializer_prompt.md - tells the agent how many features to create

        We only signal spec_complete when BOTH files are verified on disk.
        """
        if not self.client:
            return

        # Build the message content
        if attachments and len(attachments) > 0:
            # Multimodal message: build content blocks array
            content_blocks = []

            # Add text block if there's text
            if message:
                content_blocks.append({"type": "text", "text": message})

            # Add image blocks
            for att in attachments:
                content_blocks.append({
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": att.mimeType,
                        "data": att.base64Data,
                    }
                })

            # Send multimodal content to Claude using async generator format
            # The SDK's query() accepts AsyncIterable[dict] for custom message formats
            await self.client.query(_make_multimodal_message(content_blocks))
            logger.info(f"Sent multimodal message with {len(attachments)} image(s)")
        else:
            # Text-only message: use string format
            await self.client.query(message)

        current_text = ""

        # Track pending writes for BOTH required files
        pending_writes = {
            "app_spec": None,      # {"tool_id": ..., "path": ...}
            "initializer": None,   # {"tool_id": ..., "path": ...}
        }

        # Track which files have been successfully written
        files_written = {
            "app_spec": False,
            "initializer": False,
        }

        # Store paths for the completion message
        spec_path = None

        # Stream the response using receive_response
        async for msg in self.client.receive_response():
            msg_type = type(msg).__name__

            if msg_type == "AssistantMessage" and hasattr(msg, "content"):
                # Process content blocks in the assistant message
                for block in msg.content:
                    block_type = type(block).__name__

                    if block_type == "TextBlock" and hasattr(block, "text"):
                        # Accumulate text and yield it
                        text = block.text
                        if text:
                            current_text += text
                            yield {"type": "text", "content": text}

                            # Store in message history
                            self.messages.append({
                                "role": "assistant",
                                "content": text,
                                "timestamp": datetime.now().isoformat()
                            })

                    elif block_type == "ToolUseBlock" and hasattr(block, "name"):
                        tool_name = block.name
                        tool_input = getattr(block, "input", {})
                        tool_id = getattr(block, "id", "")

                        if tool_name in ("Write", "Edit"):
                            # File being written or edited - track for verification
                            file_path = tool_input.get("file_path", "")

                            # Track app_spec.txt
                            if "app_spec.txt" in str(file_path):
                                pending_writes["app_spec"] = {
                                    "tool_id": tool_id,
                                    "path": file_path
                                }
                                logger.info(f"{tool_name} tool called for app_spec.txt: {file_path}")

                            # Track initializer_prompt.md
                            elif "initializer_prompt.md" in str(file_path):
                                pending_writes["initializer"] = {
                                    "tool_id": tool_id,
                                    "path": file_path
                                }
                                logger.info(f"{tool_name} tool called for initializer_prompt.md: {file_path}")

            elif msg_type == "UserMessage" and hasattr(msg, "content"):
                # Tool results - check for write confirmations and errors
                for block in msg.content:
                    block_type = type(block).__name__
                    if block_type == "ToolResultBlock":
                        is_error = getattr(block, "is_error", False)
                        tool_use_id = getattr(block, "tool_use_id", "")

                        if is_error:
                            content = getattr(block, "content", "Unknown error")
                            logger.warning(f"Tool error: {content}")
                            # Clear any pending writes that failed
                            for key in pending_writes:
                                if pending_writes[key] and tool_use_id == pending_writes[key].get("tool_id"):
                                    logger.error(f"{key} write failed: {content}")
                                    pending_writes[key] = None
                        else:
                            # Tool succeeded - check which file was written

                            # Check app_spec.txt
                            if pending_writes["app_spec"] and tool_use_id == pending_writes["app_spec"].get("tool_id"):
                                file_path = pending_writes["app_spec"]["path"]
                                full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
                                if full_path.exists():
                                    logger.info(f"app_spec.txt verified at: {full_path}")
                                    files_written["app_spec"] = True
                                    spec_path = file_path

                                    # Notify about file write (but NOT completion yet)
                                    yield {
                                        "type": "file_written",
                                        "path": str(file_path)
                                    }
                                else:
                                    logger.error(f"app_spec.txt not found after write: {full_path}")
                                pending_writes["app_spec"] = None

                            # Check initializer_prompt.md
                            if pending_writes["initializer"] and tool_use_id == pending_writes["initializer"].get("tool_id"):
                                file_path = pending_writes["initializer"]["path"]
                                full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
                                if full_path.exists():
                                    logger.info(f"initializer_prompt.md verified at: {full_path}")
                                    files_written["initializer"] = True

                                    # Notify about file write
                                    yield {
                                        "type": "file_written",
                                        "path": str(file_path)
                                    }
                                else:
                                    logger.error(f"initializer_prompt.md not found after write: {full_path}")
                                pending_writes["initializer"] = None

                            # Check if BOTH files are now written - only then signal completion
                            if files_written["app_spec"] and files_written["initializer"]:
                                logger.info("Both app_spec.txt and initializer_prompt.md verified - signaling completion")
                                self.complete = True
                                yield {
                                    "type": "spec_complete",
                                    "path": str(spec_path)
                                }

    def is_complete(self) -> bool:
        """Check if spec creation is complete."""
        return self.complete

    def get_messages(self) -> list[dict]:
        """Get all messages in the conversation."""
        return self.messages.copy()

# Session registry with thread safety
_sessions: dict[str, SpecChatSession] = {}
_sessions_lock = threading.Lock()

def get_session(project_name: str) -> Optional[SpecChatSession]:
    """Get an existing session for a project."""
    with _sessions_lock:
        return _sessions.get(project_name)

async def create_session(
    project_name: str,
    project_dir: Path,
    is_import: bool = False,
    codebase_analysis: Optional[str] = None,
) -> SpecChatSession:
    """Create a new session for a project, closing any existing one.

    Args:
        project_name: Name of the project
        project_dir: Absolute path to the project directory
        is_import: If True, use import-spec skill for existing codebases
        codebase_analysis: Formatted analysis content for import mode
    """
    old_session: Optional[SpecChatSession] = None

    with _sessions_lock:
        # Get existing session to close later (outside the lock)
        old_session = _sessions.pop(project_name, None)
        session = SpecChatSession(
            project_name,
            project_dir,
            is_import=is_import,
            codebase_analysis=codebase_analysis,
        )
        _sessions[project_name] = session

    # Close old session outside the lock to avoid blocking
    if old_session:
        try:
            await old_session.close()
        except Exception as e:
            logger.warning(f"Error closing old session for {project_name}: {e}")

    return session

async def remove_session(project_name: str) -> None:
    """Remove and close a session."""
    session: Optional[SpecChatSession] = None

    with _sessions_lock:
        session = _sessions.pop(project_name, None)

    # Close session outside the lock
    if session:
        try:
            await session.close()
        except Exception as e:
            logger.warning(f"Error closing session for {project_name}: {e}")

def list_sessions() -> list[str]:
    """List all active session project names."""
    with _sessions_lock:
        return list(_sessions.keys())

async def cleanup_all_sessions() -> None:
    """Close all active sessions. Called on server shutdown."""
    sessions_to_close: list[SpecChatSession] = []

    with _sessions_lock:
        sessions_to_close = list(_sessions.values())
        _sessions.clear()

    for session in sessions_to_close:
        try:
            await session.close()
        except Exception as e:
            logger.warning(f"Error closing session {session.project_name}: {e}")


Perfect! I've now read all the service files. Let me compile comprehensive documentation for each service.

---

## COMPREHENSIVE SERVICE DOCUMENTATION

### **1. credential_resolver.py**

**Class: `CredentialResolver`**

**Purpose:**
Parses and manages credentials from a `credentials.md` file in markdown format. Provides credential lookup, caching, and context generation for agents and services.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `get(service_name)` | Get all credentials for a service by name (case-insensitive, ignores parenthetical descriptions) |
| `get_field(service_name, field_name)` | Get specific field from service credentials (e.g., password, host, api_token) |
| `resolve_ref(credential_ref)` | Resolve credential references in formats "service_name.field" or "service_field" |
| `get_ssh_config(host_alias)` | Get SSH connection config (host, user, port, password/key) |
| `get_api_config(service_name)` | Get API configuration (url, api_token/api_key, auth fields) |
| `list_services(category)` | List all services, optionally filtered by category |
| `list_categories()` | Get all credential categories |
| `search(query)` | Search credentials by keyword (service names, hosts, URLs) |
| `to_context_string(services)` | Generate formatted markdown for agent prompts (redacts secrets) |
| `reload()` | Reload credentials from disk |

**Integration:**
- Used by `ResourceManager`, `TerminalSession` for credential resolution
- Singleton pattern via `get_resolver()` function
- Default path: `~/claude-projects/shared-data/credentials.md`

**Configuration:**
- Field name aliases normalize common variations (username→user, pwd→password, etc.)
- Section names normalized (removes parentheticals, case-insensitive)

---

### **2. feature_graph.py**

**Class: `FeatureGraph`**

**Purpose:**
Tracks feature dependencies and file conflicts to determine which features can execute in parallel. Maintains a separate SQLite database (`feature_graph.db`) alongside the main features database.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `get_parallelizable_features(count, exclude_in_progress)` | Get N features that can run in parallel without conflicts |
| `record_files_touched(feature_id, files)` | Record which files a feature modifies (for conflict detection) |
| `predict_files_for_feature(feature_id, category, name)` | Predict files a feature will touch based on category/name heuristics |
| `set_estimated_complexity(feature_id, complexity)` | Set complexity score (1-5) for a feature |
| `get_feature_conflicts(feature_id)` | Get all features that conflict with a given feature |
| `clear_feature_data(feature_id)` | Clear all graph data when feature completes |
| `get_stats()` | Get statistics (tracked features, active locks, unique files) |

**Data Classes:**
- `ParallelFeature`: Feature with parallelism metadata (id, priority, category, name, complexity)
- `FeatureConflict`: Describes why two features cannot run in parallel (file conflicts)

**Integration:**
- Reads from main `features.db` to get pending features
- Maintains separate `feature_graph.db` for dependency tracking
- Works with `FileLockManager` to prevent file conflicts

---

### **3. file_lock.py**

**Class: `FileLockManager`**

**Purpose:**
Async-safe file locking to prevent concurrent modifications by parallel agents. Supports atomic multi-file locks and timeout-based expiration.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `acquire(file_path, feature_id, agent_id, timeout_seconds, wait)` | Acquire lock on a file with optional waiting |
| `acquire_multiple(file_paths, feature_id, ...)` | Acquire locks on multiple files atomically |
| `release(file_path, feature_id)` | Release a lock |
| `release_all(feature_id)` | Release all locks held by a feature |
| `force_release(file_path)` | Force-release lock (recovery only) |
| `is_locked(file_path)` | Check if file is locked and by which feature |
| `get_locks_for_feature(feature_id)` | Get all locks held by a feature |
| `get_all_locks()` | Get all current locks |
| `cleanup_expired()` | Clean up expired locks |
| `get_stats()` | Get lock manager statistics |

**Data Classes:**
- `LockInfo`: File lock metadata (file_path, feature_id, agent_id, locked_at, expires_at)
- `LockResult`: Result of lock operation (success, message, lock_info)

**Features:**
- Default timeout: 1 hour
- Async/await compatible with asyncio.Lock for thread safety
- Lock expiration prevents deadlocks
- Same feature can extend its own locks

---

### **4. git_operations.py**

**Class: `GitOperations`**

**Purpose:**
Low-level async git operations for feature implementation and repo management. Wraps git commands asynchronously.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `is_repo()` | Check if directory is a git repository |
| `status()` | Get detailed git status (branch, staged, unstaged, untracked, conflicts) |
| `diff(staged, file)` | Get diff output (staged or unstaged changes) |
| `log(count, file, since)` | Get commit history |
| `add(files)` | Stage files for commit |
| `commit(message, files, amend)` | Create a commit |
| `push(remote, branch, force, set_upstream)` | Push commits to remote |
| `pull(remote, branch, rebase)` | Pull changes from remote |
| `fetch(remote)` | Fetch changes without merging |
| `checkout(branch, create)` | Checkout a branch |
| `create_branch(name, start_point)` | Create a new branch |
| `stash(message)` | Stash changes |
| `stash_pop()` | Pop most recent stash |
| `reset(mode, commit)` | Reset repository state (soft/mixed/hard) |
| `get_changed_files_since(commit)` | List files changed since a commit |
| `get_file_at_commit(file, commit)` | Get file contents at specific commit |

**Data Classes:**
- `GitStatus`: Repository status (branch, ahead/behind, staged/unstaged/untracked files, conflicts)
- `GitCommit`: Commit metadata (hash, message, author, date, files_changed)
- `GitResult`: Operation result (success, output, error, commit_hash)

**Integration:**
- Async-safe with timeout support (30s default, 60s for push/pull)
- Cross-platform via asyncio.create_subprocess_exec

---

### **5. resource_manager.py**

**Class: `ResourceManager`**

**Purpose:**
Manages global and project-specific resources (servers, services, databases) with credential references. Supports SSH verification, HTTP service health checks, and capability probing.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `get_merged_resources(project_name, project_dir)` | Get global + project resources (project overrides global) |
| `add_resource(resource, project_name, project_dir)` | Add resource to global or project scope |
| `remove_resource(resource_name, scope, ...)` | Remove a resource |
| `get_resource(resource_name, ...)` | Get specific resource by name |
| `verify_resource(resource, probe_capabilities)` | Verify resource is accessible + probe for capabilities |
| `verify_all(project_name, project_dir, ...)` | Verify all resources concurrently |
| `_verify_ssh_server(resource, probe_capabilities)` | Verify SSH connectivity and probe tools |
| `_verify_http_service(resource)` | Verify HTTP service with curl |
| `_probe_ssh_capabilities(user_host, port, capabilities)` | Probe for installed tools (rust, cargo, node, python, docker, etc.) |
| `to_context_string(project_name, project_dir, include_credentials)` | Generate markdown context for prompts |

**Data Classes:**
- `Resource`: Server/service/database definition (name, type, scope, host, port, url, auth, capabilities)
- `VerificationResult`: Result of resource verification (success, message, capabilities, response_time_ms)
- `ResourceAuth`: Auth configuration (type: ssh-key/password/api-token/basic, credential_ref)
- `ResourceScope`: Enum (GLOBAL, PROJECT)
- `ResourceType`: Enum (SERVER, SERVICE, DATABASE, REPOSITORY)
- `AuthType`: Enum (SSH_KEY, SSH_PASSWORD, API_TOKEN, API_KEY, BASIC_AUTH, NONE)

**Storage:**
- Global: `~/.autocoder/global_resources.json`
- Project: `{project_dir}/resources.json`

**Capability Probes:**
rust, cargo, node, npm, python, docker, git, make, gcc, go, java, systemd, nginx, postgres, mysql, redis

---

### **6. smart_scheduler.py**

**Class: `SmartScheduler`**

**Purpose:**
Intelligent scheduling system that dynamically determines optimal parallelism based on feature analysis, historical data, and system state. Learns from past executions.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `analyze_feature(feature)` | Analyze single feature (complexity, files, risk, duration) |
| `make_decision(features, in_progress_count)` | Make scheduling decision with reasoning |
| `record_result(feature_id, feature_name, category, success, duration, files, parallelism)` | Record feature execution result |
| `get_stats()` | Get scheduler statistics (success rate, avg duration, failure streak) |

**Internal Methods:**
- `_predict_files()`: Predict files based on category and name patterns
- `_estimate_duration()`: Estimate duration from history and complexity
- `_calculate_similarity()`: Calculate feature similarity (0-1) for conflict detection

**Data Classes:**
- `FeatureAnalysis`: Single feature analysis (complexity_score 1-10, predicted_files, risk_factors, should_run_alone)
- `SchedulerDecision`: Full scheduling decision (parallelism, feature_ids, reasoning, confidence, risk_level, duration, warnings)
- `HistoricalRecord`: Past execution record (success, duration, files_touched, parallelism_level)

**Decision Factors:**
1. Feature complexity (steps, description length, keywords)
2. Predicted file conflicts (category-based heuristics)
3. Historical performance (past durations, failure rates)
4. Current system state (in-progress count, failure streak)
5. Feature clustering (similarity detection)

**Risk Keywords:** refactor, migrate, breaking, schema, database, init, setup, config, security, auth

**Conflict Categories:** database↔migration, database↔schema, config↔setup, auth↔security

**Safe Parallel Categories:** ui, frontend, component, style, test, docs

---

### **7. terminal_session.py**

**Class: `TerminalSession`**

**Purpose:**
Manages interactive Claude terminal sessions for resource management, infrastructure discovery, and configuration. Provides full system access (no sandbox) for infrastructure setup tasks.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `start()` | Initialize session and send greeting |
| `send_message(user_message)` | Send user message and stream Claude's response |
| `close()` | Clean up and close Claude client |
| `get_resources()` | Get current resources for the project |

**Internal Methods:**
- `_get_project_context()`: Generate project context for system prompt
- `_query_claude()`: Query Claude and stream responses, detect resource file writes

**Features:**
- Full system access: No sandbox, Bash(*) allowed
- Auto-refresh resources when saved
- Detects SSH/connection verification results
- Supports command execution transparency

**Message Types (yields):**
- `text`: Claude's response text
- `resource_updated`: Resources.json file written
- `resources`: Current resource list
- `command_executing`: Command being run (for transparency)
- `connection_verified`: Successful connection test
- `error`: Error message

**Integration:**
- Uses `CredentialResolver` and `ResourceManager` for resource operations
- Credentials referenced via `credential_ref` in resources.json
- Session registry with thread-safe access

---

### **8. process_manager.py**

**Class: `AgentProcessManager`**

**Purpose:**
Manages lifecycle of agent subprocesses per project. Provides start/stop/pause/resume with cross-platform support via psutil. Handles output streaming and health checks.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `start(yolo_mode)` | Start agent subprocess with optional YOLO mode |
| `stop()` | Stop agent gracefully (SIGTERM → SIGKILL) |
| `pause()` | Pause agent using psutil (cross-platform) |
| `resume()` | Resume paused agent |
| `healthcheck()` | Check if process is alive, update status to 'crashed' if dead |
| `add_output_callback(callback)` | Register callback for output lines |
| `remove_output_callback(callback)` | Remove output callback |
| `add_status_callback(callback)` | Register callback for status changes |
| `remove_status_callback(callback)` | Remove status callback |
| `get_status_dict()` | Get current status as dictionary |

**Features:**
- Prevents multiple agents per project using `.agent.lock` file
- Sanitizes output (redacts API keys, tokens, passwords)
- Supports multiple concurrent WebSocket callbacks
- YOLO mode for rapid prototyping (skips testing)
- Graceful shutdown with 5-second timeout

**Status Values:** stopped, running, paused, crashed

**Sensitive Data Redaction:**
- Anthropic API keys (sk-...)
- GitHub tokens (ghp_, gho_, ghs_, ghr_)
- AWS keys and secrets
- Generic api_key, token, password, secret patterns

**Integration:**
- Subprocess output piped to callbacks in real-time
- Lock file prevents concurrent instances
- Cross-platform pause/resume via psutil

---

### **9. assistant_chat_session.py**

**Class: `AssistantChatSession`**

**Purpose:**
Manages read-only conversational assistant sessions for projects. Claude can analyze code but cannot modify files.

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `start()` | Initialize session and send greeting |
| `send_message(user_message)` | Send user message and stream response |
| `close()` | Clean up Claude client |
| `get_conversation_id()` | Get current conversation ID |

**Module Functions:**
- `get_session(project_name)`: Get existing session
- `create_session(project_name, project_dir, conversation_id)`: Create/replace session
- `remove_session(project_name)`: Close and remove session
- `list_sessions()`: List active sessions
- `cleanup_all_sessions()`: Close all sessions (on shutdown)

**Features:**
- Read-only access: Read, Glob, Grep, WebFetch, WebSearch only
- Feature MCP tools read-only: get_stats, get_next, get_for_regression (no mark_passing, skip, create_bulk)
- Persists conversation history to SQLite
- Uses Claude Opus 4.5 for best comprehension
- Loads app_spec.txt for project context

**Tools Available:**
- Read, Glob, Grep: Code analysis
- WebFetch, WebSearch: Online documentation
- Feature tools: Check progress and pending work

**Message Types (yields):**
- `conversation_created`: New conversation initialized
- `text`: Claude's response text
- `tool_call`: Tool usage (for debugging)
- `response_done`: Response complete
- `error`: Error message

**Integration:**
- Thread-safe session registry
- Conversation database: `{project_dir}/.conversations.db`
- No editing capabilities - guides users to main agent for modifications

---

### **10. spec_chat_session.py**

**Class: `SpecChatSession`**

**Purpose:**
Manages interactive spec creation conversation with Claude. Guides users through multi-phase specification development or codebase import. Supports multimodal input (images).

**Key Methods:**

| Method | Purpose |
|--------|---------|
| `start()` | Initialize session and begin spec creation |
| `send_message(user_message, attachments)` | Send user message/images and stream response |
| `close()` | Clean up Claude client |
| `is_complete()` | Check if spec creation is complete |
| `get_messages()` | Get all conversation messages |

**Module Functions:**
- `get_session(project_name)`: Get existing session
- `create_session(project_name, project_dir, is_import, codebase_analysis)`: Create/replace session
- `remove_session(project_name)`: Close and remove session
- `list_sessions()`: List active sessions
- `cleanup_all_sessions()`: Close all sessions (on shutdown)

**Spec Creation Phases (Create Mode):**
1. Project Overview (name, description, audience)
2. Involvement Level (Quick vs Detailed mode)
3. Technology Preferences
4. Features (main exploration)
5. Technical Details (derived or discussed)
6. Success Criteria & Approval

**Import Mode:**
- Present codebase analysis
- Document existing features
- Identify gaps and plan new features

**Features:**
- Multimodal support: Text + images
- Automatic file writing to app_spec.txt and initializer_prompt.md
- Validates both required files written before signaling completion
- Uses create-spec.md or import-spec.md skills from `.claude/commands/`
- Auto-approves file writes (acceptEdits mode)
- Uses Claude Opus 4.5 for quality

**Tools Available:**
- Read, Write, Edit, Glob: File operations
- No Bash, WebFetch, WebSearch in spec creation

**Completion Requirements:**
- BOTH app_spec.txt AND initializer_prompt.md must be written
- Only signals `spec_complete` when both verified on disk

**Message Types (yields):**
- `text`: Claude's response
- `file_written`: File creation notification
- `spec_complete`: Spec creation finished
- `response_done`: Response streaming complete
- `error`: Error message

**Message Storage:**
- Stores user/assistant messages in session memory
- Tracks timestamps and attachments
- Accessible via `get_messages()`

---

## INTEGRATION SUMMARY

```
┌─────────────────────────────────────────────────────────────┐
│                  SERVICE INTERACTIONS                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  CredentialResolver ◄──────┐                                │
│         ▲                    │                               │
│         │                    └─── ResourceManager            │
│         │                           ▲                        │
│         └───────────────────────────┘                        │
│                      │                                       │
│                      ▼                                       │
│        TerminalSession (interactive resource mgmt)          │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │              File Conflict Management                   ││
│  │                                                         ││
│  │  FileLockManager ◄─── FeatureGraph                     ││
│  │      ▲                     ▲                            ││
│  │      │                     │                            ││
│  │      └─────────────────────┘                            ││
│  │                                                         ││
│  │  SmartScheduler ─────► Coordination                    ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │           Subprocess & Git Management                   ││
│  │                                                         ││
│  │  AgentProcessManager ◄──── GitOperations              ││
│  │      ▲                                                  ││
│  │      │                                                  ││
│  │  (handles output streaming & status callbacks)         ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │              Chat Sessions                              ││
│  │                                                         ││
│  │  AssistantChatSession  ◄──── Project Analysis          ││
│  │  SpecChatSession       ◄──── App Specification         ││
│  │  TerminalSession       ◄──── Infrastructure Setup      ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```

---

## CONFIGURATION & INITIALIZATION

All services support singleton or registry patterns for instance management:

| Service | Initialization | Singleton Function |
|---------|-----------------|-------------------|
| CredentialResolver | `CredentialResolver(path)` | `get_resolver(path)` |
| ResourceManager | `ResourceManager(creds_path, resources_path)` | `get_manager()` |
| FileLockManager | `FileLockManager(project_dir)` | `get_lock_manager(project_dir)` |
| AgentProcessManager | `AgentProcessManager(project_name, project_dir, root_dir)` | `get_manager(project_name, ...)` |
| TerminalSession | `TerminalSession(project_name, project_dir)` | `get_or_create_terminal_session()` |
| AssistantChatSession | `AssistantChatSession(project_name, project_dir)` | `create_session()` |
| SpecChatSession | `SpecChatSession(project_name, project_dir, is_import)` | `create_session()` |

---

**END OF DOCUMENTATION**