"""
Git Agent
=========

Dedicated agent responsible for all version control operations:
- Track changes made by coding agents
- Create logical, well-structured commits
- Push completed work to remote
- Pull code when review is needed
- Handle merge conflicts
"""

import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Optional, Callable, Any

from ..services.git_operations import GitOperations, GitStatus, GitResult

logger = logging.getLogger(__name__)

@dataclass
class ChangeEntry:
    """A tracked change made by a coding agent."""
    file: str
    agent_id: str
    feature_id: int
    change_type: str  # "created", "modified", "deleted"
    timestamp: datetime = field(default_factory=datetime.utcnow)
    summary: Optional[str] = None

@dataclass
class GitAgentConfig:
    """Configuration for the Git Agent."""
    auto_commit: bool = True
    auto_push: bool = True
    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    branch_strategy: str = "main"  # "main", "feature"
    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])
    conflict_auto_resolve: list[str] = field(default_factory=lambda: ["whitespace", "imports"])

class GitAgent:
    """
    Dedicated agent for version control operations.

    The Git Agent:
    1. Tracks changes made by coding agents during feature implementation
    2. Groups related changes into logical commits
    3. Generates meaningful commit messages
    4. Pushes completed work to the remote repository
    5. Provides code review context by pulling and summarizing changes
    """

    def __init__(
        self,
        project_dir: Path,
        config: Optional[GitAgentConfig] = None,
        on_commit: Optional[Callable[[str, str], Any]] = None,
        on_push: Optional[Callable[[bool, str], Any]] = None,
    ):
        """
        Initialize the Git Agent.

        Args:
            project_dir: Path to the project directory (git repo)
            config: Agent configuration
            on_commit: Callback when a commit is made (hash, message)
            on_push: Callback when a push completes (success, output)
        """
        self.project_dir = project_dir
        self.config = config or GitAgentConfig()
        self.git = GitOperations(project_dir)
        self.change_log: list[ChangeEntry] = []
        self._on_commit = on_commit
        self._on_push = on_push
        self._lock = asyncio.Lock()

    async def track_change(
        self,
        file: str,
        agent_id: str,
        feature_id: int,
        change_type: str = "modified",
        summary: Optional[str] = None,
    ) -> None:
        """
        Record a change made by a coding agent.

        This should be called by the orchestrator when a coding agent
        modifies a file during feature implementation.

        Args:
            file: Path to the changed file (relative to project)
            agent_id: ID of the coding agent that made the change
            feature_id: ID of the feature being implemented
            change_type: Type of change (created, modified, deleted)
            summary: Optional description of the change
        """
        async with self._lock:
            entry = ChangeEntry(
                file=file,
                agent_id=agent_id,
                feature_id=feature_id,
                change_type=change_type,
                summary=summary,
            )
            self.change_log.append(entry)
            logger.debug(f"Tracked change: {file} ({change_type}) for feature {feature_id}")

    async def on_feature_complete(
        self,
        feature_id: int,
        feature_name: Optional[str] = None,
    ) -> Optional[GitResult]:
        """
        Handle feature completion - commit and optionally push changes.

        Called by the orchestrator when a coding agent completes a feature.

        Args:
            feature_id: ID of the completed feature
            feature_name: Optional name of the feature

        Returns:
            GitResult if a commit was made, None if no changes
        """
        if not self.config.auto_commit:
            return None

        async with self._lock:
            # Get changes for this feature
            feature_changes = [
                c for c in self.change_log if c.feature_id == feature_id
            ]

            if not feature_changes:
                logger.info(f"No tracked changes for feature {feature_id}")
                return None

            # Generate commit message
            message = self._generate_commit_message(
                feature_id, feature_name, feature_changes
            )

            # Get unique files
            files = list(set(c.file for c in feature_changes))

            # Create commit
            result = await self.git.commit(message, files=files)

            if result.success:
                logger.info(f"Committed feature {feature_id}: {result.commit_hash}")

                # Callback
                if self._on_commit and result.commit_hash:
                    self._on_commit(result.commit_hash, message)

                # Clear tracked changes for this feature
                self.change_log = [
                    c for c in self.change_log if c.feature_id != feature_id
                ]

                # Auto-push if configured
                if self.config.auto_push and "feature_complete" in self.config.push_on:
                    push_result = await self.git.push()
                    if self._on_push:
                        self._on_push(push_result.success, push_result.output)

            else:
                logger.error(f"Commit failed for feature {feature_id}: {result.error}")

            return result

    async def on_session_end(self) -> Optional[GitResult]:
        """
        Handle session end - commit any remaining changes.

        Called when the autocoder session ends to ensure no work is lost.

        Returns:
            GitResult if a commit was made, None if no changes
        """
        if not self.change_log:
            return None

        async with self._lock:
            # Group remaining changes by feature
            features = set(c.feature_id for c in self.change_log)

            if len(features) == 1:
                # Single feature - commit normally
                feature_id = list(features)[0]
                return await self.on_feature_complete(feature_id)
            else:
                # Multiple features - create a summary commit
                message = self._generate_session_end_message(self.change_log)
                files = list(set(c.file for c in self.change_log))

                result = await self.git.commit(message, files=files)

                if result.success:
                    self.change_log.clear()

                    if self.config.auto_push and "session_end" in self.config.push_on:
                        push_result = await self.git.push()
                        if self._on_push:
                            self._on_push(push_result.success, push_result.output)

                return result

    async def provide_review_context(
        self,
        files: Optional[list[str]] = None,
        since_commit: Optional[str] = None,
    ) -> str:
        """
        Pull latest and provide context for code review.

        Called when a coding agent needs to review existing code
        before making changes.

        Args:
            files: Optional list of files to focus on
            since_commit: Optional commit to compare from

        Returns:
            Formatted context string with relevant changes
        """
        # Pull before review if configured
        if "code_review" in self.config.pull_before:
            await self.git.pull()

        lines = ["## Code Review Context", ""]

        # Get recent commits
        commits = await self.git.log(count=5)
        if commits:
            lines.append("### Recent Commits")
            for commit in commits:
                lines.append(f"- `{commit.short_hash}`: {commit.message}")
            lines.append("")

        # Get diff if since_commit specified
        if since_commit:
            changed_files = await self.git.get_changed_files_since(since_commit)
            if changed_files:
                lines.append(f"### Files Changed Since {since_commit[:7]}")
                for f in changed_files[:20]:  # Limit to 20 files
                    lines.append(f"- `{f}`")
                if len(changed_files) > 20:
                    lines.append(f"- ... and {len(changed_files) - 20} more")
                lines.append("")

        # Get current status
        status = await self.git.status()
        if not status.is_clean:
            lines.append("### Uncommitted Changes")
            if status.staged:
                lines.append("**Staged:**")
                for f in status.staged[:10]:
                    lines.append(f"- `{f}`")
            if status.unstaged:
                lines.append("**Unstaged:**")
                for f in status.unstaged[:10]:
                    lines.append(f"- `{f}`")
            lines.append("")

        # Get diff for specific files if requested
        if files:
            lines.append("### Relevant File Changes")
            for file in files[:5]:  # Limit to 5 files
                diff = await self.git.diff(file=file)
                if diff:
                    lines.append(f"#### `{file}`")
                    # Truncate long diffs
                    if len(diff) > 1000:
                        diff = diff[:1000] + "\n... (truncated)"
                    lines.append(f"```diff\n{diff}\n```")
            lines.append("")

        return "\n".join(lines)

    async def handle_conflict(self) -> dict:
        """
        Analyze and potentially resolve merge conflicts.

        Returns:
            Dict with conflict info and resolution status
        """
        status = await self.git.status()

        if not status.has_conflicts:
            return {"has_conflicts": False}

        # Get conflicting files
        conflicts = []
        for file in status.unstaged:
            # Check if file has conflict markers
            file_path = self.project_dir / file
            if file_path.exists():
                try:
                    content = file_path.read_text(encoding="utf-8")
                    if "<<<<<<< " in content:
                        conflicts.append(file)
                except Exception:
                    pass

        return {
            "has_conflicts": True,
            "conflicting_files": conflicts,
            "auto_resolvable": self._can_auto_resolve(conflicts),
        }

    def _can_auto_resolve(self, files: list[str]) -> bool:
        """Check if conflicts can be auto-resolved."""
        # Simple heuristic - check if conflicts are in certain file types
        auto_resolve_extensions = {".lock", ".sum", ".mod"}
        return all(
            Path(f).suffix in auto_resolve_extensions
            for f in files
        )

    def _generate_commit_message(
        self,
        feature_id: int,
        feature_name: Optional[str],
        changes: list[ChangeEntry],
    ) -> str:
        """Generate a commit message for feature changes."""
        if self.config.commit_style == "simple":
            return f"Implement feature #{feature_id}" + (
                f": {feature_name}" if feature_name else ""
            )

        elif self.config.commit_style == "conventional":
            # Determine type from changes
            has_new_files = any(c.change_type == "created" for c in changes)
            commit_type = "feat" if has_new_files else "fix"

            subject = f"{commit_type}: implement feature #{feature_id}"
            if feature_name:
                subject = f"{commit_type}: {feature_name}"

            body_lines = ["", "Changes:"]
            for change in changes[:10]:  # Limit body
                action = {
                    "created": "Add",
                    "modified": "Update",
                    "deleted": "Remove",
                }.get(change.change_type, "Modify")
                body_lines.append(f"- {action} {change.file}")
                if change.summary:
                    body_lines.append(f"  {change.summary}")

            if len(changes) > 10:
                body_lines.append(f"- ... and {len(changes) - 10} more files")

            body_lines.append("")
            body_lines.append(f"Feature #{feature_id}")

            return subject + "\n" + "\n".join(body_lines)

        else:  # detailed
            lines = [f"Feature #{feature_id}: {feature_name or 'Implementation'}"]
            lines.append("")
            lines.append("## Changes")
            lines.append("")

            for change in changes:
                lines.append(f"### {change.file}")
                lines.append(f"- Type: {change.change_type}")
                lines.append(f"- Agent: {change.agent_id}")
                if change.summary:
                    lines.append(f"- Summary: {change.summary}")
                lines.append("")

            return "\n".join(lines)

    def _generate_session_end_message(self, changes: list[ChangeEntry]) -> str:
        """Generate a commit message for session end."""
        feature_ids = set(c.feature_id for c in changes)
        files = set(c.file for c in changes)

        if len(feature_ids) == 1:
            return f"WIP: Feature #{list(feature_ids)[0]} progress ({len(files)} files)"
        else:
            return f"WIP: Progress on features {', '.join(f'#{f}' for f in sorted(feature_ids))} ({len(files)} files)"

    async def get_status(self) -> GitStatus:
        """Get current git status."""
        return await self.git.status()

    def get_pending_changes(self) -> list[ChangeEntry]:
        """Get list of tracked but uncommitted changes."""
        return list(self.change_log)

    def clear_tracking(self) -> None:
        """Clear all tracked changes (use after manual commit)."""
        self.change_log.clear()


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\agents\__init__.py

"""
Autocoder Agents
================

Specialized agents for different tasks within the Autocoder system.
"""

from .git_agent import GitAgent, ChangeEntry

__all__ = ["GitAgent", "ChangeEntry"]


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\agents\git_agent.py

"""
Git Agent
=========

Dedicated agent responsible for all version control operations:
- Track changes made by coding agents
- Create logical, well-structured commits
- Push completed work to remote
- Pull code when review is needed
- Handle merge conflicts
"""

import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Optional, Callable, Any

from ..services.git_operations import GitOperations, GitStatus, GitResult

logger = logging.getLogger(__name__)

@dataclass
class ChangeEntry:
    """A tracked change made by a coding agent."""
    file: str
    agent_id: str
    feature_id: int
    change_type: str  # "created", "modified", "deleted"
    timestamp: datetime = field(default_factory=datetime.utcnow)
    summary: Optional[str] = None

@dataclass
class GitAgentConfig:
    """Configuration for the Git Agent."""
    auto_commit: bool = True
    auto_push: bool = True
    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    branch_strategy: str = "main"  # "main", "feature"
    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])
    conflict_auto_resolve: list[str] = field(default_factory=lambda: ["whitespace", "imports"])

class GitAgent:
    """
    Dedicated agent for version control operations.

    The Git Agent:
    1. Tracks changes made by coding agents during feature implementation
    2. Groups related changes into logical commits
    3. Generates meaningful commit messages
    4. Pushes completed work to the remote repository
    5. Provides code review context by pulling and summarizing changes
    """

    def __init__(
        self,
        project_dir: Path,
        config: Optional[GitAgentConfig] = None,
        on_commit: Optional[Callable[[str, str], Any]] = None,
        on_push: Optional[Callable[[bool, str], Any]] = None,
    ):
        """
        Initialize the Git Agent.

        Args:
            project_dir: Path to the project directory (git repo)
            config: Agent configuration
            on_commit: Callback when a commit is made (hash, message)
            on_push: Callback when a push completes (success, output)
        """
        self.project_dir = project_dir
        self.config = config or GitAgentConfig()
        self.git = GitOperations(project_dir)
        self.change_log: list[ChangeEntry] = []
        self._on_commit = on_commit
        self._on_push = on_push
        self._lock = asyncio.Lock()

    async def track_change(
        self,
        file: str,
        agent_id: str,
        feature_id: int,
        change_type: str = "modified",
        summary: Optional[str] = None,
    ) -> None:
        """
        Record a change made by a coding agent.

        This should be called by the orchestrator when a coding agent
        modifies a file during feature implementation.

        Args:
            file: Path to the changed file (relative to project)
            agent_id: ID of the coding agent that made the change
            feature_id: ID of the feature being implemented
            change_type: Type of change (created, modified, deleted)
            summary: Optional description of the change
        """
        async with self._lock:
            entry = ChangeEntry(
                file=file,
                agent_id=agent_id,
                feature_id=feature_id,
                change_type=change_type,
                summary=summary,
            )
            self.change_log.append(entry)
            logger.debug(f"Tracked change: {file} ({change_type}) for feature {feature_id}")

    async def on_feature_complete(
        self,
        feature_id: int,
        feature_name: Optional[str] = None,
    ) -> Optional[GitResult]:
        """
        Handle feature completion - commit and optionally push changes.

        Called by the orchestrator when a coding agent completes a feature.

        Args:
            feature_id: ID of the completed feature
            feature_name: Optional name of the feature

        Returns:
            GitResult if a commit was made, None if no changes
        """
        if not self.config.auto_commit:
            return None

        async with self._lock:
            # Get changes for this feature
            feature_changes = [
                c for c in self.change_log if c.feature_id == feature_id
            ]

            if not feature_changes:
                logger.info(f"No tracked changes for feature {feature_id}")
                return None

            # Generate commit message
            message = self._generate_commit_message(
                feature_id, feature_name, feature_changes
            )

            # Get unique files
            files = list(set(c.file for c in feature_changes))

            # Create commit
            result = await self.git.commit(message, files=files)

            if result.success:
                logger.info(f"Committed feature {feature_id}: {result.commit_hash}")

                # Callback
                if self._on_commit and result.commit_hash:
                    self._on_commit(result.commit_hash, message)

                # Clear tracked changes for this feature
                self.change_log = [
                    c for c in self.change_log if c.feature_id != feature_id
                ]

                # Auto-push if configured
                if self.config.auto_push and "feature_complete" in self.config.push_on:
                    push_result = await self.git.push()
                    if self._on_push:
                        self._on_push(push_result.success, push_result.output)

            else:
                logger.error(f"Commit failed for feature {feature_id}: {result.error}")

            return result

    async def on_session_end(self) -> Optional[GitResult]:
        """
        Handle session end - commit any remaining changes.

        Called when the autocoder session ends to ensure no work is lost.

        Returns:
            GitResult if a commit was made, None if no changes
        """
        if not self.change_log:
            return None

        async with self._lock:
            # Group remaining changes by feature
            features = set(c.feature_id for c in self.change_log)

            if len(features) == 1:
                # Single feature - commit normally
                feature_id = list(features)[0]
                return await self.on_feature_complete(feature_id)
            else:
                # Multiple features - create a summary commit
                message = self._generate_session_end_message(self.change_log)
                files = list(set(c.file for c in self.change_log))

                result = await self.git.commit(message, files=files)

                if result.success:
                    self.change_log.clear()

                    if self.config.auto_push and "session_end" in self.config.push_on:
                        push_result = await self.git.push()
                        if self._on_push:
                            self._on_push(push_result.success, push_result.output)

                return result

    async def provide_review_context(
        self,
        files: Optional[list[str]] = None,
        since_commit: Optional[str] = None,
    ) -> str:
        """
        Pull latest and provide context for code review.

        Called when a coding agent needs to review existing code
        before making changes.

        Args:
            files: Optional list of files to focus on
            since_commit: Optional commit to compare from

        Returns:
            Formatted context string with relevant changes
        """
        # Pull before review if configured
        if "code_review" in self.config.pull_before:
            await self.git.pull()

        lines = ["## Code Review Context", ""]

        # Get recent commits
        commits = await self.git.log(count=5)
        if commits:
            lines.append("### Recent Commits")
            for commit in commits:
                lines.append(f"- `{commit.short_hash}`: {commit.message}")
            lines.append("")

        # Get diff if since_commit specified
        if since_commit:
            changed_files = await self.git.get_changed_files_since(since_commit)
            if changed_files:
                lines.append(f"### Files Changed Since {since_commit[:7]}")
                for f in changed_files[:20]:  # Limit to 20 files
                    lines.append(f"- `{f}`")
                if len(changed_files) > 20:
                    lines.append(f"- ... and {len(changed_files) - 20} more")
                lines.append("")

        # Get current status
        status = await self.git.status()
        if not status.is_clean:
            lines.append("### Uncommitted Changes")
            if status.staged:
                lines.append("**Staged:**")
                for f in status.staged[:10]:
                    lines.append(f"- `{f}`")
            if status.unstaged:
                lines.append("**Unstaged:**")
                for f in status.unstaged[:10]:
                    lines.append(f"- `{f}`")
            lines.append("")

        # Get diff for specific files if requested
        if files:
            lines.append("### Relevant File Changes")
            for file in files[:5]:  # Limit to 5 files
                diff = await self.git.diff(file=file)
                if diff:
                    lines.append(f"#### `{file}`")
                    # Truncate long diffs
                    if len(diff) > 1000:
                        diff = diff[:1000] + "\n... (truncated)"
                    lines.append(f"```diff\n{diff}\n```")
            lines.append("")

        return "\n".join(lines)

    async def handle_conflict(self) -> dict:
        """
        Analyze and potentially resolve merge conflicts.

        Returns:
            Dict with conflict info and resolution status
        """
        status = await self.git.status()

        if not status.has_conflicts:
            return {"has_conflicts": False}

        # Get conflicting files
        conflicts = []
        for file in status.unstaged:
            # Check if file has conflict markers
            file_path = self.project_dir / file
            if file_path.exists():
                try:
                    content = file_path.read_text(encoding="utf-8")
                    if "<<<<<<< " in content:
                        conflicts.append(file)
                except Exception:
                    pass

        return {
            "has_conflicts": True,
            "conflicting_files": conflicts,
            "auto_resolvable": self._can_auto_resolve(conflicts),
        }

    def _can_auto_resolve(self, files: list[str]) -> bool:
        """Check if conflicts can be auto-resolved."""
        # Simple heuristic - check if conflicts are in certain file types
        auto_resolve_extensions = {".lock", ".sum", ".mod"}
        return all(
            Path(f).suffix in auto_resolve_extensions
            for f in files
        )

    def _generate_commit_message(
        self,
        feature_id: int,
        feature_name: Optional[str],
        changes: list[ChangeEntry],
    ) -> str:
        """Generate a commit message for feature changes."""
        if self.config.commit_style == "simple":
            return f"Implement feature #{feature_id}" + (
                f": {feature_name}" if feature_name else ""
            )

        elif self.config.commit_style == "conventional":
            # Determine type from changes
            has_new_files = any(c.change_type == "created" for c in changes)
            commit_type = "feat" if has_new_files else "fix"

            subject = f"{commit_type}: implement feature #{feature_id}"
            if feature_name:
                subject = f"{commit_type}: {feature_name}"

            body_lines = ["", "Changes:"]
            for change in changes[:10]:  # Limit body
                action = {
                    "created": "Add",
                    "modified": "Update",
                    "deleted": "Remove",
                }.get(change.change_type, "Modify")
                body_lines.append(f"- {action} {change.file}")
                if change.summary:
                    body_lines.append(f"  {change.summary}")

            if len(changes) > 10:
                body_lines.append(f"- ... and {len(changes) - 10} more files")

            body_lines.append("")
            body_lines.append(f"Feature #{feature_id}")

            return subject + "\n" + "\n".join(body_lines)

        else:  # detailed
            lines = [f"Feature #{feature_id}: {feature_name or 'Implementation'}"]
            lines.append("")
            lines.append("## Changes")
            lines.append("")

            for change in changes:
                lines.append(f"### {change.file}")
                lines.append(f"- Type: {change.change_type}")
                lines.append(f"- Agent: {change.agent_id}")
                if change.summary:
                    lines.append(f"- Summary: {change.summary}")
                lines.append("")

            return "\n".join(lines)

    def _generate_session_end_message(self, changes: list[ChangeEntry]) -> str:
        """Generate a commit message for session end."""
        feature_ids = set(c.feature_id for c in changes)
        files = set(c.file for c in changes)

        if len(feature_ids) == 1:
            return f"WIP: Feature #{list(feature_ids)[0]} progress ({len(files)} files)"
        else:
            return f"WIP: Progress on features {', '.join(f'#{f}' for f in sorted(feature_ids))} ({len(files)} files)"

    async def get_status(self) -> GitStatus:
        """Get current git status."""
        return await self.git.status()

    def get_pending_changes(self) -> list[ChangeEntry]:
        """Get list of tracked but uncommitted changes."""
        return list(self.change_log)

    def clear_tracking(self) -> None:
        """Clear all tracked changes (use after manual commit)."""
        self.change_log.clear()


Now let me create the git router for REST endpoints.


TodoWrite

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3.1: Create git_operations.py service

Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 3.4: Create GitPanel.tsx component

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


Write

"""
Git Router
==========

REST and WebSocket endpoints for git operations and status.
"""

import asyncio
import json
import logging
import re
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from ..services.git_operations import GitOperations, GitStatus, GitResult

logger = logging.getLogger(__name__)

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

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

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

# ============================================================================
# Response Models
# ============================================================================

class GitStatusResponse(BaseModel):
    """Git status response."""
    branch: str
    ahead: int
    behind: int
    staged: list[str]
    unstaged: list[str]
    untracked: list[str]
    has_conflicts: bool
    is_clean: bool
    remote: Optional[str]
    last_commit_hash: Optional[str]
    last_commit_message: Optional[str]
    last_commit_time: Optional[str]

class GitCommitResponse(BaseModel):
    """Git commit info."""
    hash: str
    short_hash: str
    message: str
    author: str
    date: str

class GitResultResponse(BaseModel):
    """Generic git operation result."""
    success: bool
    output: str
    error: Optional[str]
    commit_hash: Optional[str]

class CommitRequest(BaseModel):
    """Request to create a commit."""
    message: str
    files: Optional[list[str]] = None

class PushRequest(BaseModel):
    """Request to push commits."""
    remote: str = "origin"
    branch: Optional[str] = None
    force: bool = False
    set_upstream: bool = False

class PullRequest(BaseModel):
    """Request to pull changes."""
    remote: str = "origin"
    branch: Optional[str] = None
    rebase: bool = False

class CheckoutRequest(BaseModel):
    """Request to checkout a branch."""
    branch: str
    create: bool = False

# ============================================================================
# REST Endpoints
# ============================================================================

@router.get("/projects/{project_name}/status", response_model=GitStatusResponse)
async def get_status(project_name: str):
    """Get git status for a project."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    status = await git.status()

    return GitStatusResponse(
        branch=status.branch,
        ahead=status.ahead,
        behind=status.behind,
        staged=status.staged,
        unstaged=status.unstaged,
        untracked=status.untracked,
        has_conflicts=status.has_conflicts,
        is_clean=status.is_clean,
        remote=status.remote,
        last_commit_hash=status.last_commit_hash,
        last_commit_message=status.last_commit_message,
        last_commit_time=status.last_commit_time,
    )

@router.get("/projects/{project_name}/log", response_model=list[GitCommitResponse])
async def get_log(
    project_name: str,
    count: int = 10,
    file: Optional[str] = None,
):
    """Get commit history for a project."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    commits = await git.log(count=min(count, 100), file=file)

    return [
        GitCommitResponse(
            hash=c.hash,
            short_hash=c.short_hash,
            message=c.message,
            author=c.author,
            date=c.date,
        )
        for c in commits
    ]

@router.get("/projects/{project_name}/diff")
async def get_diff(
    project_name: str,
    staged: bool = False,
    file: Optional[str] = None,
):
    """Get diff of changes."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    diff = await git.diff(staged=staged, file=file)

    return {"diff": diff}

@router.post("/projects/{project_name}/add", response_model=GitResultResponse)
async def stage_files(project_name: str, files: Optional[list[str]] = None):
    """Stage files for commit."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.add(files)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/commit", response_model=GitResultResponse)
async def create_commit(project_name: str, request: CommitRequest):
    """Create a commit."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.commit(request.message, files=request.files)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/push", response_model=GitResultResponse)
async def push_commits(project_name: str, request: Optional[PushRequest] = None):
    """Push commits to remote."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    if request is None:
        request = PushRequest()

    result = await git.push(
        remote=request.remote,
        branch=request.branch,
        force=request.force,
        set_upstream=request.set_upstream,
    )

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/pull", response_model=GitResultResponse)
async def pull_changes(project_name: str, request: Optional[PullRequest] = None):
    """Pull changes from remote."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    if request is None:
        request = PullRequest()

    result = await git.pull(
        remote=request.remote,
        branch=request.branch,
        rebase=request.rebase,
    )

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/fetch", response_model=GitResultResponse)
async def fetch_changes(project_name: str, remote: str = "origin"):
    """Fetch changes from remote without merging."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.fetch(remote)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/checkout", response_model=GitResultResponse)
async def checkout_branch(project_name: str, request: CheckoutRequest):
    """Checkout a branch."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.checkout(request.branch, create=request.create)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

# ============================================================================
# WebSocket Endpoint for Real-time Updates
# ============================================================================

@router.websocket("/projects/{project_name}/ws")
async def git_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for real-time git status updates.

    Message protocol:

    Client -> Server:
    - {"type": "subscribe"} - Subscribe to git status updates
    - {"type": "unsubscribe"} - Unsubscribe from updates
    - {"type": "refresh"} - Force refresh status
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "status", "data": {...}} - Git status update
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """
    if not validate_project_name(project_name):
        await websocket.close(code=4000, reason="Invalid project name")
        return

    project_dir = _get_project_path(project_name)
    if not project_dir:
        await websocket.close(code=4004, reason="Project not found")
        return

    await websocket.accept()

    git = GitOperations(project_dir)
    subscribed = False
    poll_task: Optional[asyncio.Task] = None

    async def poll_status():
        """Poll git status and send updates."""
        last_status = None
        while True:
            try:
                status = await git.status()
                status_dict = {
                    "branch": status.branch,
                    "ahead": status.ahead,
                    "behind": status.behind,
                    "staged": status.staged,
                    "unstaged": status.unstaged,
                    "untracked": status.untracked,
                    "has_conflicts": status.has_conflicts,
                    "is_clean": status.is_clean,
                }

                # Only send if changed
                if status_dict != last_status:
                    await websocket.send_json({
                        "type": "status",
                        "data": status_dict,
                    })
                    last_status = status_dict

                await asyncio.sleep(2)  # Poll every 2 seconds

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error polling git status: {e}")
                await asyncio.sleep(5)

    try:
        while True:
            try:
                data = await websocket.receive_text()
                message = json.loads(data)
                msg_type = message.get("type")

                if msg_type == "ping":
                    await websocket.send_json({"type": "pong"})

                elif msg_type == "subscribe":
                    if not subscribed:
                        subscribed = True
                        poll_task = asyncio.create_task(poll_status())

                elif msg_type == "unsubscribe":
                    if poll_task:
                        poll_task.cancel()
                        poll_task = None
                    subscribed = False

                elif msg_type == "refresh":
                    status = await git.status()
                    await websocket.send_json({
                        "type": "status",
                        "data": {
                            "branch": status.branch,
                            "ahead": status.ahead,
                            "behind": status.behind,
                            "staged": status.staged,
                            "unstaged": status.unstaged,
                            "untracked": status.untracked,
                            "has_conflicts": status.has_conflicts,
                            "is_clean": status.is_clean,
                        },
                    })

                else:
                    await websocket.send_json({
                        "type": "error",
                        "content": f"Unknown message type: {msg_type}"
                    })

            except json.JSONDecodeError:
                await websocket.send_json({
                    "type": "error",
                    "content": "Invalid JSON message"
                })

    except WebSocketDisconnect:
        logger.info(f"Git WebSocket disconnected for project: {project_name}")
    except Exception as e:
        logger.exception(f"Git WebSocket error for project {project_name}: {e}")
    finally:
        if poll_task:
            poll_task.cancel()


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

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3.1: Create git_operations.py service

Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 3.4: Create GitPanel.tsx component

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates

Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\git.py

"""
Git Router
==========

REST and WebSocket endpoints for git operations and status.
"""

import asyncio
import json
import logging
import re
from pathlib import Path
from typing import Optional

from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from ..services.git_operations import GitOperations, GitStatus, GitResult

logger = logging.getLogger(__name__)

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

def _get_project_path(project_name: str) -> Path:
    """Get project path from registry."""
    import sys
    root = Path(__file__).parent.parent.parent
    if str(root) not in sys.path:
        sys.path.insert(0, str(root))

    from registry import get_project_path
    return get_project_path(project_name)

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

# ============================================================================
# Response Models
# ============================================================================

class GitStatusResponse(BaseModel):
    """Git status response."""
    branch: str
    ahead: int
    behind: int
    staged: list[str]
    unstaged: list[str]
    untracked: list[str]
    has_conflicts: bool
    is_clean: bool
    remote: Optional[str]
    last_commit_hash: Optional[str]
    last_commit_message: Optional[str]
    last_commit_time: Optional[str]

class GitCommitResponse(BaseModel):
    """Git commit info."""
    hash: str
    short_hash: str
    message: str
    author: str
    date: str

class GitResultResponse(BaseModel):
    """Generic git operation result."""
    success: bool
    output: str
    error: Optional[str]
    commit_hash: Optional[str]

class CommitRequest(BaseModel):
    """Request to create a commit."""
    message: str
    files: Optional[list[str]] = None

class PushRequest(BaseModel):
    """Request to push commits."""
    remote: str = "origin"
    branch: Optional[str] = None
    force: bool = False
    set_upstream: bool = False

class PullRequest(BaseModel):
    """Request to pull changes."""
    remote: str = "origin"
    branch: Optional[str] = None
    rebase: bool = False

class CheckoutRequest(BaseModel):
    """Request to checkout a branch."""
    branch: str
    create: bool = False

# ============================================================================
# REST Endpoints
# ============================================================================

@router.get("/projects/{project_name}/status", response_model=GitStatusResponse)
async def get_status(project_name: str):
    """Get git status for a project."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    status = await git.status()

    return GitStatusResponse(
        branch=status.branch,
        ahead=status.ahead,
        behind=status.behind,
        staged=status.staged,
        unstaged=status.unstaged,
        untracked=status.untracked,
        has_conflicts=status.has_conflicts,
        is_clean=status.is_clean,
        remote=status.remote,
        last_commit_hash=status.last_commit_hash,
        last_commit_message=status.last_commit_message,
        last_commit_time=status.last_commit_time,
    )

@router.get("/projects/{project_name}/log", response_model=list[GitCommitResponse])
async def get_log(
    project_name: str,
    count: int = 10,
    file: Optional[str] = None,
):
    """Get commit history for a project."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    commits = await git.log(count=min(count, 100), file=file)

    return [
        GitCommitResponse(
            hash=c.hash,
            short_hash=c.short_hash,
            message=c.message,
            author=c.author,
            date=c.date,
        )
        for c in commits
    ]

@router.get("/projects/{project_name}/diff")
async def get_diff(
    project_name: str,
    staged: bool = False,
    file: Optional[str] = None,
):
    """Get diff of changes."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    diff = await git.diff(staged=staged, file=file)

    return {"diff": diff}

@router.post("/projects/{project_name}/add", response_model=GitResultResponse)
async def stage_files(project_name: str, files: Optional[list[str]] = None):
    """Stage files for commit."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.add(files)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/commit", response_model=GitResultResponse)
async def create_commit(project_name: str, request: CommitRequest):
    """Create a commit."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.commit(request.message, files=request.files)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/push", response_model=GitResultResponse)
async def push_commits(project_name: str, request: Optional[PushRequest] = None):
    """Push commits to remote."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    if request is None:
        request = PushRequest()

    result = await git.push(
        remote=request.remote,
        branch=request.branch,
        force=request.force,
        set_upstream=request.set_upstream,
    )

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/pull", response_model=GitResultResponse)
async def pull_changes(project_name: str, request: Optional[PullRequest] = None):
    """Pull changes from remote."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    if request is None:
        request = PullRequest()

    result = await git.pull(
        remote=request.remote,
        branch=request.branch,
        rebase=request.rebase,
    )

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/fetch", response_model=GitResultResponse)
async def fetch_changes(project_name: str, remote: str = "origin"):
    """Fetch changes from remote without merging."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.fetch(remote)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

@router.post("/projects/{project_name}/checkout", response_model=GitResultResponse)
async def checkout_branch(project_name: str, request: CheckoutRequest):
    """Checkout a branch."""
    if not validate_project_name(project_name):
        raise HTTPException(status_code=400, detail="Invalid project name")

    project_dir = _get_project_path(project_name)
    if not project_dir:
        raise HTTPException(status_code=404, detail="Project not found")

    git = GitOperations(project_dir)

    if not await git.is_repo():
        raise HTTPException(status_code=400, detail="Not a git repository")

    result = await git.checkout(request.branch, create=request.create)

    return GitResultResponse(
        success=result.success,
        output=result.output,
        error=result.error,
        commit_hash=result.commit_hash,
    )

# ============================================================================
# WebSocket Endpoint for Real-time Updates
# ============================================================================

@router.websocket("/projects/{project_name}/ws")
async def git_websocket(websocket: WebSocket, project_name: str):
    """
    WebSocket endpoint for real-time git status updates.

    Message protocol:

    Client -> Server:
    - {"type": "subscribe"} - Subscribe to git status updates
    - {"type": "unsubscribe"} - Unsubscribe from updates
    - {"type": "refresh"} - Force refresh status
    - {"type": "ping"} - Keep-alive ping

    Server -> Client:
    - {"type": "status", "data": {...}} - Git status update
    - {"type": "error", "content": "..."} - Error message
    - {"type": "pong"} - Keep-alive pong
    """
    if not validate_project_name(project_name):
        await websocket.close(code=4000, reason="Invalid project name")
        return

    project_dir = _get_project_path(project_name)
    if not project_dir:
        await websocket.close(code=4004, reason="Project not found")
        return

    await websocket.accept()

    git = GitOperations(project_dir)
    subscribed = False
    poll_task: Optional[asyncio.Task] = None

    async def poll_status():
        """Poll git status and send updates."""
        last_status = None
        while True:
            try:
                status = await git.status()
                status_dict = {
                    "branch": status.branch,
                    "ahead": status.ahead,
                    "behind": status.behind,
                    "staged": status.staged,
                    "unstaged": status.unstaged,
                    "untracked": status.untracked,
                    "has_conflicts": status.has_conflicts,
                    "is_clean": status.is_clean,
                }

                # Only send if changed
                if status_dict != last_status:
                    await websocket.send_json({
                        "type": "status",
                        "data": status_dict,
                    })
                    last_status = status_dict

                await asyncio.sleep(2)  # Poll every 2 seconds

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error polling git status: {e}")
                await asyncio.sleep(5)

    try:
        while True:
            try:
                data = await websocket.receive_text()
                message = json.loads(data)
                msg_type = message.get("type")

                if msg_type == "ping":
                    await websocket.send_json({"type": "pong"})

                elif msg_type == "subscribe":
                    if not subscribed:
                        subscribed = True
                        poll_task = asyncio.create_task(poll_status())

                elif msg_type == "unsubscribe":
                    if poll_task:
                        poll_task.cancel()
                        poll_task = None
                    subscribed = False

                elif msg_type == "refresh":
                    status = await git.status()
                    await websocket.send_json({
                        "type": "status",
                        "data": {
                            "branch": status.branch,
                            "ahead": status.ahead,
                            "behind": status.behind,
                            "staged": status.staged,
                            "unstaged": status.unstaged,
                            "untracked": status.untracked,
                            "has_conflicts": status.has_conflicts,
                            "is_clean": status.is_clean,
                        },
                    })

                else:
                    await websocket.send_json({
                        "type": "error",
                        "content": f"Unknown message type: {msg_type}"
                    })

            except json.JSONDecodeError:
                await websocket.send_json({
                    "type": "error",
                    "content": "Invalid JSON message"
                })

    except WebSocketDisconnect:
        logger.info(f"Git WebSocket disconnected for project: {project_name}")
    except Exception as e:
        logger.exception(f"Git WebSocket error for project {project_name}: {e}")
    finally:
        if poll_task:
            poll_task.cancel()


Now I need to register the git router in main.py and routers/__init__.py. Let me update those files.


Now let me register the git router.


Edit

from .terminal import router as terminal_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
]

from .terminal import router as terminal_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
]


from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
)

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
)


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\routers\__init__.py has been updated successfully.

from .terminal import router as terminal_router
from .git import router as git_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
    "git_router",
]

"""
API Routers
===========

FastAPI routers for different API endpoints.
"""

from .agent import router as agent_router
from .assistant_chat import router as assistant_chat_router
from .features import router as features_router
from .filesystem import router as filesystem_router
from .projects import router as projects_router
from .spec_creation import router as spec_creation_router
from .terminal import router as terminal_router

__all__ = [
    "projects_router",
    "features_router",
    "agent_router",
    "spec_creation_router",
    "filesystem_router",
    "assistant_chat_router",
    "terminal_router",
]


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\server\main.py has been updated successfully.

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
)

"""
FastAPI Main Application
========================

Main entry point for the Autonomous Coding UI server.
Provides REST API, WebSocket, and static file serving.
"""

import shutil
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
)
from .schemas import SetupStatus
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
from .services.process_manager import cleanup_all_managers
from .websocket import project_websocket

# Paths
ROOT_DIR = Path(__file__).parent.parent
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    yield
    # Shutdown - cleanup all running agents and assistant sessions
    await cleanup_all_managers()
    await cleanup_assistant_sessions()

# Create FastAPI app
app = FastAPI(
    title="Autonomous Coding UI",
    description="Web UI for the Autonomous Coding Agent",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS - allow only localhost origins for security
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",      # Vite dev server
        "http://127.0.0.1:5173",
        "http://localhost:8888",      # Production
        "http://127.0.0.1:8888",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Include Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(features_router)
app.include_router(agent_router)
app.include_router(spec_creation_router)
app.include_router(filesystem_router)
app.include_router(assistant_chat_router)
app.include_router(terminal_router)

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@app.websocket("/ws/projects/{project_name}")
async def websocket_endpoint(websocket: WebSocket, project_name: str):
    """WebSocket endpoint for real-time project updates."""
    await project_websocket(websocket, project_name)

# ============================================================================
# Setup & Health Endpoints
# ============================================================================

@app.get("/api/health")
async def health_check():
    """Health check endpoint."""
    return {"status": "healthy"}

@app.get("/api/setup/status", response_model=SetupStatus)
async def setup_status():
    """Check system setup status."""
    # Check for Claude CLI
    claude_cli = shutil.which("claude") is not None

    # Check for credentials file
    credentials_path = Path.home() / ".claude" / ".credentials.json"
    credentials = credentials_path.exists()

    # Check for Node.js and npm
    node = shutil.which("node") is not None
    npm = shutil.which("npm") is not None

    return SetupStatus(
        claude_cli=claude_cli,
        credentials=credentials,
        node=node,
        npm=npm,
    )

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if UI_DIST_DIR.exists():
    # Mount static assets
    app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(UI_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """
        Serve static files or fall back to index.html for SPA routing.
        """
        # Check if the path is an API route (shouldn't hit this due to router ordering)
        if path.startswith("api/") or path.startswith("ws/"):
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = UI_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(UI_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "server.main:app",
        host="127.0.0.1",  # Localhost only for security
        port=8888,
        reload=True,
    )


"""
FastAPI Main Application
========================

Main entry point for the Autonomous Coding UI server.
Provides REST API, WebSocket, and static file serving.
"""

import shutil
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, HTTPException, Request, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

from .routers import (
    agent_router,
    assistant_chat_router,
    features_router,
    filesystem_router,
    projects_router,
    spec_creation_router,
    terminal_router,
    git_router,
)
from .schemas import SetupStatus
from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
from .services.process_manager import cleanup_all_managers
from .websocket import project_websocket

# Paths
ROOT_DIR = Path(__file__).parent.parent
UI_DIST_DIR = ROOT_DIR / "ui" / "dist"

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifespan context manager for startup and shutdown."""
    # Startup
    yield
    # Shutdown - cleanup all running agents and assistant sessions
    await cleanup_all_managers()
    await cleanup_assistant_sessions()

# Create FastAPI app
app = FastAPI(
    title="Autonomous Coding UI",
    description="Web UI for the Autonomous Coding Agent",
    version="1.0.0",
    lifespan=lifespan,
)

# CORS - allow only localhost origins for security
app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "http://localhost:5173",      # Vite dev server
        "http://127.0.0.1:5173",
        "http://localhost:8888",      # Production
        "http://127.0.0.1:8888",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ============================================================================
# Security Middleware
# ============================================================================

@app.middleware("http")
async def require_localhost(request: Request, call_next):
    """Only allow requests from localhost."""
    client_host = request.client.host if request.client else None

    # Allow localhost connections
    if client_host not in ("127.0.0.1", "::1", "localhost", None):
        raise HTTPException(status_code=403, detail="Localhost access only")

    return await call_next(request)

# ============================================================================
# Include Routers
# ============================================================================

app.include_router(projects_router)
app.include_router(features_router)
app.include_router(agent_router)
app.include_router(spec_creation_router)
app.include_router(filesystem_router)
app.include_router(assistant_chat_router)
app.include_router(terminal_router)

# ============================================================================
# WebSocket Endpoint
# ============================================================================

@app.websocket("/ws/projects/{project_name}")
async def websocket_endpoint(websocket: WebSocket, project_name: str):
    """WebSocket endpoint for real-time project updates."""
    await project_websocket(websocket, project_name)

# ============================================================================
# Setup & Health Endpoints
# ============================================================================

@app.get("/api/health")
async def health_check():
    """Health check endpoint."""
    return {"status": "healthy"}

@app.get("/api/setup/status", response_model=SetupStatus)
async def setup_status():
    """Check system setup status."""
    # Check for Claude CLI
    claude_cli = shutil.which("claude") is not None

    # Check for credentials file
    credentials_path = Path.home() / ".claude" / ".credentials.json"
    credentials = credentials_path.exists()

    # Check for Node.js and npm
    node = shutil.which("node") is not None
    npm = shutil.which("npm") is not None

    return SetupStatus(
        claude_cli=claude_cli,
        credentials=credentials,
        node=node,
        npm=npm,
    )

# ============================================================================
# Static File Serving (Production)
# ============================================================================

# Serve React build files if they exist
if UI_DIST_DIR.exists():
    # Mount static assets
    app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")

    @app.get("/")
    async def serve_index():
        """Serve the React app index.html."""
        return FileResponse(UI_DIST_DIR / "index.html")

    @app.get("/{path:path}")
    async def serve_spa(path: str):
        """
        Serve static files or fall back to index.html for SPA routing.
        """
        # Check if the path is an API route (shouldn't hit this due to router ordering)
        if path.startswith("api/") or path.startswith("ws/"):
            raise HTTPException(status_code=404)

        # Try to serve the file directly
        file_path = UI_DIST_DIR / path
        if file_path.exists() and file_path.is_file():
            return FileResponse(file_path)

        # Fall back to index.html for SPA routing
        return FileResponse(UI_DIST_DIR / "index.html")

# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "server.main:app",
        host="127.0.0.1",  # Localhost only for security
        port=8888,
        reload=True,
    )


Conversation compacted


This session is being continued from a previous conversation that ran out of context. The conversation is summarized below:
Analysis:
Let me chronologically analyze the conversation to capture all important details:

1. Initial Request: User ran `/sync` command to sync Claude Code preferences from Gitea
   - Checked git repos, pulled from origin (SSH had permission issues)
   - Listed available slash commands: /context, /s, /save, /sync
   - Read recent session logs showing work on Autocoder+ redesign

2. User's First Major Request: Add a Claude terminal to Autocoder for nuanced tasks
   - For adding resources like server locations, password lists
   - Claude should figure out how to connect and save to project/resource list
   - Store as global resources for all projects

3. User Feedback on Design Questions:
   - 1) Reference credentials.md (not store separately)
   - 2) Verify connections when starting work
   - 3) Probe as deep as needed for tasks being performed
   - 4) Merge project + global resources

4. Second Major Request: Add Git Agent
   - Dedicated agent for commits to Gitea
   - Track changes and push when completed
   - Handle pulls for code review requests

5. Planning Phase: Created comprehensive plan at `C:\Users\MikeSwanson\.claude\plans\floofy-pondering-sphinx.md`
   - Phase 1: Foundation - Remove sandbox, credential resolver, resource manager
   - Phase 2: Claude Terminal
   - Phase 3: Git Agent
   - Phase 4: Parallel coding agents
   - Phase 5: Integration

6. Implementation Progress:
   - Phase 1.1: Disabled sandbox in client.py
   - Phase 1.2: Expanded allowed commands in security.py (added ssh, scp, rsync, etc.)
   - Phase 1.3: Created credential_resolver.py
   - Phase 1.4: Created resource_manager.py
   - Phase 1.5: Added context injection to prompts.py
   - Phase 2.1: Created terminal_session.py
   - Phase 2.2: Created terminal.py router
   - Phase 2.3: Created useTerminal.ts hook
   - Phase 2.4: Created TerminalChat.tsx component
   - Phase 3.1: Created git_operations.py
   - Phase 3.2: Created git_agent.py
   - Phase 3.3: Created git.py router (just completed)

Let me document all the files created/modified and important code patterns.

Summary:
1. Primary Request and Intent:
   The user wants to transform the existing Autocoder into "Autocoder 2.0" with:
   - **Claude Terminal**: An accessible terminal for nuanced tasks like adding server locations, password lists, etc. Claude should figure out how to connect to resources and save them to project or global resource lists.
   - **Git Agent**: A dedicated agent responsible for commits to Gitea - tracking changes, pushing when complete, and handling pulls for code review.
   - **Remove Sandbox Constraints**: Full system access (SSH, infrastructure, etc.)
   - **Context Injection**: Agents should know about credentials.md, server infrastructure, CLAUDE.md
   - **Parallel Agents**: Multiple coding agents working simultaneously (pending)

   User's specific answers to design questions:
   - Credentials: Reference credentials.md (not store separately)
   - Verification: Verify when starting work
   - Capability probing: As deep as needed for tasks
   - Resource scope: Merge global + project resources

2. Key Technical Concepts:
   - Claude Agent SDK (`claude_agent_sdk`)
   - FastAPI WebSocket patterns
   - React hooks for WebSocket connections
   - Git operations via async subprocess
   - Credential parsing from Markdown
   - Resource management with JSON storage
   - Context injection into agent prompts
   - Neobrutalism design system (Tailwind CSS v4)

3. Files and Code Sections:

   **Modified Files:**

   - `autocoder-master/client.py`
     - Disabled sandbox for full system access
     ```python
     security_settings = {
         # Sandbox disabled for full system access (SSH, infrastructure, etc.)
         "sandbox": {"enabled": False},
         ...
     }
     ```

   - `autocoder-master/security.py`
     - Expanded ALLOWED_COMMANDS to include: ssh, scp, rsync, wget, python, cargo, docker, etc.
     - Now ~90+ commands allowed for infrastructure access

   - `autocoder-master/prompts.py`
     - Added context injection functions: `get_infrastructure_context()`, `inject_context_into_prompt()`, `get_coding_prompt_with_context()`

   - `server/services/__init__.py`
     - Added exports for new services

   - `server/routers/__init__.py`
     - Added terminal_router and git_router exports

   - `server/main.py`
     - Registered terminal_router and git_router

   **Created Files:**

   - `server/services/credential_resolver.py`
     - Parses `credentials.md` Markdown format
     - Key methods: `get()`, `get_field()`, `resolve_ref()`, `get_ssh_config()`, `get_api_config()`
     - Pattern: `### ServiceName` for sections, `- **Key:** value` for fields

   - `server/services/resource_manager.py`
     - Manages global (~/.autocoder/global_resources.json) and project (project/resources.json) resources
     - Dataclasses: `Resource`, `ResourceAuth`, `VerificationResult`
     - Enums: `ResourceScope`, `ResourceType`, `AuthType`
     - Key methods: `get_merged_resources()`, `add_resource()`, `verify_resource()`, `verify_all()`

   - `server/services/terminal_session.py`
     - WebSocket-based Claude session for resource management
     - Full system access (no sandbox)
     - System prompt guides resource discovery workflow

   - `server/routers/terminal.py`
     - REST endpoints: `/api/terminal/sessions`, `/api/terminal/resources/{project_name}`
     - WebSocket: `/api/terminal/ws/{project_name}`

   - `server/services/git_operations.py`
     - Async git command wrappers
     - Dataclasses: `GitStatus`, `GitCommit`, `GitResult`
     - Methods: `status()`, `commit()`, `push()`, `pull()`, `diff()`, `log()`, etc.

   - `server/agents/__init__.py` and `server/agents/git_agent.py`
     - Git Agent for tracking changes and coordinating commits
     - `ChangeEntry` dataclass for tracking file changes
     - `GitAgentConfig` for configuration
     - Key methods: `track_change()`, `on_feature_complete()`, `on_session_end()`, `provide_review_context()`

   - `server/routers/git.py`
     - REST endpoints for git operations: status, log, diff, add, commit, push, pull, fetch, checkout
     - WebSocket for real-time git status polling

   - `ui/src/hooks/useTerminal.ts`
     - React hook for terminal WebSocket connection
     - Exports: `useTerminal()`, `TerminalMessage`, `Resource`

   - `ui/src/components/TerminalChat.tsx`
     - Full chat UI with resource sidebar panel
     - Uses ReactMarkdown for message rendering

4. Errors and fixes:
   - SSH key permission denied when pulling from gitea remote
     - Used HTTPS origin remote instead (already working)
   - No major code errors encountered during implementation

5. Problem Solving:
   - Adapted existing patterns from `spec_chat_session.py` and `useSpecChat.ts` for terminal implementation
   - Created comprehensive service layer for credentials and resources
   - Built async git operations wrapper for the Git Agent

6. All user messages:
   - `/sync` command to sync Claude Code preferences
   - "For the autocoder project: A claude terminal should be accesible for nuanced tasks. For instance when adding resources like server locations, password lists, etc. Claude should be able to figure out how to connect to those resources and save it to the project/resouce list for future work on the current project, or even store them as global resour es for all projects.."
   - "1) reference credentials.md 2) Verify when starting work 3) As deep as needed for the tasks it will be performing 4) Merge"
   - "Also add another agent that is solely responsible for commits to gitea. It can track changes and make sure they are pushed. When things are completed, push them. And it can also handle pulls if necessary when a code review is called for by another agent."
   - "Let's start planning."

7. Pending Tasks:
   - Phase 3.4: Create GitPanel.tsx component (frontend git status UI)
   - Phase 4: Parallel coding agents (feature graph, file locking, parallel runner)
   - Phase 5: Integration and UI updates (App.tsx, configuration)

8. Current Work:
   Just completed Phase 3.3 - creating the git.py router with REST and WebSocket endpoints. The router was registered in `server/main.py` and `server/routers/__init__.py`.

   Current todo state shows:
   - Phase 3.3: Create git.py router - `in_progress` (just completed edits)
   - Phase 3.4: Create GitPanel.tsx component - `pending`

9. Optional Next Step:
   Create the GitPanel.tsx frontend component for displaying git status. This is the next item in Phase 3 after the git router was just completed.

   From the plan:
   > **New File:** `ui/src/components/GitPanel.tsx`
   > - Branch indicator
   > - Changed files list (staged/unstaged)
   > - Commit history (recent)
   > - Push/pull buttons
   > - Auto-refresh on file changes.

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: C:\Users\MikeSwanson\.claude\projects\C--Users-MikeSwanson-claude-projects\9b0508e7-d6ae-4a06-aed2-926ed6af56b0.jsonl
Please continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on.


The user wants me to continue from where we left off. Based on the summary, I just completed Phase 3.3 (creating the git.py router) and the next task is Phase 3.4: Create GitPanel.tsx component.

Let me update the todo list to mark 3.3 as complete and start on 3.4, then create the GitPanel.tsx component.

Looking at the existing code structure and the plan, I need to create:
- `ui/src/components/GitPanel.tsx` - A panel showing git status with:
  - Branch indicator
  - Changed files list (staged/unstaged)
  - Commit history (recent)
  - Push/pull buttons
  - Auto-refresh on file changes

I should also create a `useGit.ts` hook similar to useTerminal.ts for managing git state and WebSocket connections.

Let me first check what UI patterns exist in the codebase to match the style.


I'll continue with Phase 3.4 - creating the GitPanel.tsx component. Let me first update the todo list and then create the necessary frontend files.


TodoWrite

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3.1: Create git_operations.py service

Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 3.4: Create GitPanel.tsx component

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates

Phase 3.2: Create git_agent.py agent

Phase 3.3: Create git.py router

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


Now let me check the existing UI components to understand the styling patterns being used, then create the GitPanel component and a useGit hook.


Let me check the existing UI component patterns to ensure consistency.


Good - I can see the existing components and hooks. Let me look at a panel component to see the styling patterns, and the useTerminal hook as a reference for the git hook.


Let me read the existing AssistantPanel component to understand the panel styling pattern.


1→/**
     2→ * Assistant Panel Component
     3→ *
     4→ * Slide-in panel container for the project assistant chat.
     5→ * Slides in from the right side of the screen.
     6→ */
     7→
     8→import { X, Bot } from 'lucide-react'
     9→import { AssistantChat } from './AssistantChat'
    10→
    11→interface AssistantPanelProps {
    12→  projectName: string
    13→  isOpen: boolean
    14→  onClose: () => void
    15→}
    16→
    17→export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelProps) {
    18→  return (
    19→    <>
    20→      {/* Backdrop - click to close */}
    21→      {isOpen && (
    22→

    27→      )}
    28→
    29→      {/* Panel */}
    30→      <div
    31→        className={`
    32→          fixed right-0 top-0 bottom-0 z-50
    33→          w-[400px] max-w-[90vw]
    34→          bg-white
    35→          border-l-4 border-[var(--color-neo-border)]
    36→          shadow-[-8px_0_0px_rgba(0,0,0,1)]
    37→          transform transition-transform duration-300 ease-out
    38→          flex flex-col
    39→          ${isOpen ? 'translate-x-0' : 'translate-x-full'}
    40→        `}
    41→        role="dialog"
    42→        aria-label="Project Assistant"
    43→        aria-hidden={!isOpen}
    44→      >
    45→        {/* Header */}
    46→

    47→

    48→

    49→              <Bot size={18} />
    50→

    51→

    52→
Project Assistant

    53→
{projectName}

    54→

    55→

    56→          <button
    57→            onClick={onClose}
    58→            className="
    59→              neo-btn neo-btn-ghost
    60→              p-2
    61→              bg-white/20 border-white/40
    62→              hover:bg-white/30
    63→              text-white
    64→            "
    65→            title="Close Assistant (Press A)"
    66→            aria-label="Close Assistant"
    67→          >
    68→            <X size={18} />
    69→          </button>
    70→

    71→
    72→        {/* Chat area */}
    73→

    74→          {isOpen && <AssistantChat projectName={projectName} />}
    75→

    76→

    77→    </>
    78→  )
    79→}
    80→

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

/**
 * Assistant Panel Component
 *
 * Slide-in panel container for the project assistant chat.
 * Slides in from the right side of the screen.
 */

import { X, Bot } from 'lucide-react'
import { AssistantChat } from './AssistantChat'

interface AssistantPanelProps {
  projectName: string
  isOpen: boolean
  onClose: () => void
}

export function AssistantPanel({ projectName, isOpen, onClose }: AssistantPanelProps) {
  return (
    <>
      {/* Backdrop - click to close */}
      {isOpen && (

      )}

      {/* Panel */}

        {/* Header */}

              <Bot size={18} />

Project Assistant

{projectName}

          <button
            onClick={onClose}
            className="
              neo-btn neo-btn-ghost
              p-2
              bg-white/20 border-white/40
              hover:bg-white/30
              text-white
            "
            title="Close Assistant (Press A)"
            aria-label="Close Assistant"
          >
            <X size={18} />
          </button>

        {/* Chat area */}

          {isOpen && <AssistantChat projectName={projectName} />}

    </>
  )
}