     1→"""
     2→Agent Process Manager
     3→=====================
     4→
     5→Manages the lifecycle of agent subprocesses per project.
     6→Provides start/stop/pause/resume functionality with cross-platform support.
     7→"""
     8→
     9→import asyncio
    10→import logging
    11→import re
    12→import subprocess
    13→import sys
    14→import threading
    15→from datetime import datetime
    16→from pathlib import Path
    17→from typing import Awaitable, Callable, Literal, Set
    18→
    19→import psutil
    20→
    21→logger = logging.getLogger(__name__)
    22→
    23→# Patterns for sensitive data that should be redacted from output
    24→SENSITIVE_PATTERNS = [
    25→    r'sk-[a-zA-Z0-9]{20,}',  # Anthropic API keys
    26→    r'ANTHROPIC_API_KEY=[^\s]+',
    27→    r'api[_-]?key[=:][^\s]+',
    28→    r'token[=:][^\s]+',
    29→    r'password[=:][^\s]+',
    30→    r'secret[=:][^\s]+',
    31→    r'ghp_[a-zA-Z0-9]{36,}',  # GitHub personal access tokens
    32→    r'gho_[a-zA-Z0-9]{36,}',  # GitHub OAuth tokens
    33→    r'ghs_[a-zA-Z0-9]{36,}',  # GitHub server tokens
    34→    r'ghr_[a-zA-Z0-9]{36,}',  # GitHub refresh tokens
    35→    r'aws[_-]?access[_-]?key[=:][^\s]+',  # AWS keys
    36→    r'aws[_-]?secret[=:][^\s]+',
    37→]
    38→
    39→
    40→def sanitize_output(line: str) -> str:
    41→    """Remove sensitive information from output lines."""
    42→    for pattern in SENSITIVE_PATTERNS:
    43→        line = re.sub(pattern, '[REDACTED]', line, flags=re.IGNORECASE)
    44→    return line
    45→
    46→
    47→class AgentProcessManager:
    48→    """
    49→    Manages agent subprocess lifecycle for a single project.
    50→
    51→    Provides start/stop/pause/resume with cross-platform support via psutil.
    52→    Supports multiple output callbacks for WebSocket clients.
    53→    """
    54→
    55→    def __init__(
    56→        self,
    57→        project_name: str,
    58→        project_dir: Path,
    59→        root_dir: Path,
    60→    ):
    61→        """
    62→        Initialize the process manager.
    63→
    64→        Args:
    65→            project_name: Name of the project
    66→            project_dir: Absolute path to the project directory
    67→            root_dir: Root directory of the autonomous-coding-ui project
    68→        """
    69→        self.project_name = project_name
    70→        self.project_dir = project_dir
    71→        self.root_dir = root_dir
    72→        self.process: subprocess.Popen | None = None
    73→        self._status: Literal["stopped", "running", "paused", "crashed"] = "stopped"
    74→        self.started_at: datetime | None = None
    75→        self._output_task: asyncio.Task | None = None
    76→        self.yolo_mode: bool = False  # YOLO mode for rapid prototyping
    77→
    78→        # Support multiple callbacks (for multiple WebSocket clients)
    79→        self._output_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
    80→        self._status_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
    81→        self._callbacks_lock = threading.Lock()
    82→
    83→        # Lock file to prevent multiple instances (stored in project directory)
    84→        self.lock_file = self.project_dir / ".agent.lock"
    85→
    86→    @property
    87→    def status(self) -> Literal["stopped", "running", "paused", "crashed"]:
    88→        return self._status
    89→
    90→    @status.setter
    91→    def status(self, value: Literal["stopped", "running", "paused", "crashed"]):
    92→        old_status = self._status
    93→        self._status = value
    94→        if old_status != value:
    95→            self._notify_status_change(value)
    96→
    97→    def _notify_status_change(self, status: str) -> None:
    98→        """Notify all registered callbacks of status change."""
    99→        with self._callbacks_lock:
   100→            callbacks = list(self._status_callbacks)
   101→
   102→        for callback in callbacks:
   103→            try:
   104→                # Schedule the callback in the event loop
   105→                loop = asyncio.get_running_loop()
   106→                loop.create_task(self._safe_callback(callback, status))
   107→            except RuntimeError:
   108→                # No running event loop
   109→                pass
   110→
   111→    async def _safe_callback(self, callback: Callable, *args) -> None:
   112→        """Safely execute a callback, catching and logging any errors."""
   113→        try:
   114→            await callback(*args)
   115→        except Exception as e:
   116→            logger.warning(f"Callback error: {e}")
   117→
   118→    def add_output_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
   119→        """Add a callback for output lines."""
   120→        with self._callbacks_lock:
   121→            self._output_callbacks.add(callback)
   122→
   123→    def remove_output_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
   124→        """Remove an output callback."""
   125→        with self._callbacks_lock:
   126→            self._output_callbacks.discard(callback)
   127→
   128→    def add_status_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
   129→        """Add a callback for status changes."""
   130→        with self._callbacks_lock:
   131→            self._status_callbacks.add(callback)
   132→
   133→    def remove_status_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
   134→        """Remove a status callback."""
   135→        with self._callbacks_lock:
   136→            self._status_callbacks.discard(callback)
   137→
   138→    @property
   139→    def pid(self) -> int | None:
   140→        return self.process.pid if self.process else None
   141→
   142→    def _check_lock(self) -> bool:
   143→        """Check if another agent is already running for this project."""
   144→        if not self.lock_file.exists():
   145→            return True
   146→
   147→        try:
   148→            pid = int(self.lock_file.read_text().strip())
   149→            if psutil.pid_exists(pid):
   150→                # Check if it's actually our agent process
   151→                try:
   152→                    proc = psutil.Process(pid)
   153→                    cmdline = " ".join(proc.cmdline())
   154→                    if "autonomous_agent_demo.py" in cmdline:
   155→                        return False  # Another agent is running
   156→                except (psutil.NoSuchProcess, psutil.AccessDenied):
   157→                    pass
   158→            # Stale lock file
   159→            self.lock_file.unlink(missing_ok=True)
   160→            return True
   161→        except (ValueError, OSError):
   162→            self.lock_file.unlink(missing_ok=True)
   163→            return True
   164→
   165→    def _create_lock(self) -> None:
   166→        """Create lock file with current process PID."""
   167→        self.lock_file.parent.mkdir(parents=True, exist_ok=True)
   168→        if self.process:
   169→            self.lock_file.write_text(str(self.process.pid))
   170→
   171→    def _remove_lock(self) -> None:
   172→        """Remove lock file."""
   173→        self.lock_file.unlink(missing_ok=True)
   174→
   175→    async def _broadcast_output(self, line: str) -> None:
   176→        """Broadcast output line to all registered callbacks."""
   177→        with self._callbacks_lock:
   178→            callbacks = list(self._output_callbacks)
   179→
   180→        for callback in callbacks:
   181→            await self._safe_callback(callback, line)
   182→
   183→    async def _stream_output(self) -> None:
   184→        """Stream process output to callbacks."""
   185→        if not self.process or not self.process.stdout:
   186→            return
   187→
   188→        try:
   189→            loop = asyncio.get_running_loop()
   190→            while True:
   191→                # Use run_in_executor for blocking readline
   192→                line = await loop.run_in_executor(
   193→                    None, self.process.stdout.readline
   194→                )
   195→                if not line:
   196→                    break
   197→
   198→                decoded = line.decode("utf-8", errors="replace").rstrip()
   199→                sanitized = sanitize_output(decoded)
   200→
   201→                await self._broadcast_output(sanitized)
   202→
   203→        except asyncio.CancelledError:
   204→            raise
   205→        except Exception as e:
   206→            logger.warning(f"Output streaming error: {e}")
   207→        finally:
   208→            # Check if process ended
   209→            if self.process and self.process.poll() is not None:
   210→                exit_code = self.process.returncode
   211→                if exit_code != 0 and self.status == "running":
   212→                    self.status = "crashed"
   213→                elif self.status == "running":
   214→                    self.status = "stopped"
   215→                self._remove_lock()
   216→
   217→    async def start(self, yolo_mode: bool = False) -> tuple[bool, str]:
   218→        """
   219→        Start the agent as a subprocess.
   220→
   221→        Args:
   222→            yolo_mode: If True, run in YOLO mode (no browser testing)
   223→
   224→        Returns:
   225→            Tuple of (success, message)
   226→        """
   227→        if self.status in ("running", "paused"):
   228→            return False, f"Agent is already {self.status}"
   229→
   230→        if not self._check_lock():
   231→            return False, "Another agent instance is already running for this project"
   232→
   233→        # Store YOLO mode for status queries
   234→        self.yolo_mode = yolo_mode
   235→
   236→        # Build command - pass absolute path to project directory
   237→        cmd = [
   238→            sys.executable,
   239→            str(self.root_dir / "autonomous_agent_demo.py"),
   240→            "--project-dir",
   241→            str(self.project_dir.resolve()),
   242→        ]
   243→
   244→        # Add --yolo flag if YOLO mode is enabled
   245→        if yolo_mode:
   246→            cmd.append("--yolo")
   247→
   248→        try:
   249→            # Start subprocess with piped stdout/stderr
   250→            # Use project_dir as cwd so Claude SDK sandbox allows access to project files
   251→            self.process = subprocess.Popen(
   252→                cmd,
   253→                stdout=subprocess.PIPE,
   254→                stderr=subprocess.STDOUT,
   255→                cwd=str(self.project_dir),
   256→            )
   257→
   258→            self._create_lock()
   259→            self.started_at = datetime.now()
   260→            self.status = "running"
   261→
   262→            # Start output streaming task
   263→            self._output_task = asyncio.create_task(self._stream_output())
   264→
   265→            return True, f"Agent started with PID {self.process.pid}"
   266→        except Exception as e:
   267→            logger.exception("Failed to start agent")
   268→            return False, f"Failed to start agent: {e}"
   269→
   270→    async def stop(self) -> tuple[bool, str]:
   271→        """
   272→        Stop the agent (SIGTERM then SIGKILL if needed).
   273→
   274→        Returns:
   275→            Tuple of (success, message)
   276→        """
   277→        if not self.process or self.status == "stopped":
   278→            return False, "Agent is not running"
   279→
   280→        try:
   281→            # Cancel output streaming
   282→            if self._output_task:
   283→                self._output_task.cancel()
   284→                try:
   285→                    await self._output_task
   286→                except asyncio.CancelledError:
   287→                    pass
   288→
   289→            # Terminate gracefully first
   290→            self.process.terminate()
   291→
   292→            # Wait up to 5 seconds for graceful shutdown
   293→            loop = asyncio.get_running_loop()
   294→            try:
   295→                await asyncio.wait_for(
   296→                    loop.run_in_executor(None, self.process.wait),
   297→                    timeout=5.0
   298→                )
   299→            except asyncio.TimeoutError:
   300→                # Force kill if still running
   301→                self.process.kill()
   302→                await loop.run_in_executor(None, self.process.wait)
   303→
   304→            self._remove_lock()
   305→            self.status = "stopped"
   306→            self.process = None
   307→            self.started_at = None
   308→            self.yolo_mode = False  # Reset YOLO mode
   309→
   310→            return True, "Agent stopped"
   311→        except Exception as e:
   312→            logger.exception("Failed to stop agent")
   313→            return False, f"Failed to stop agent: {e}"
   314→
   315→    async def pause(self) -> tuple[bool, str]:
   316→        """
   317→        Pause the agent using psutil for cross-platform support.
   318→
   319→        Returns:
   320→            Tuple of (success, message)
   321→        """
   322→        if not self.process or self.status != "running":
   323→            return False, "Agent is not running"
   324→
   325→        try:
   326→            proc = psutil.Process(self.process.pid)
   327→            proc.suspend()
   328→            self.status = "paused"
   329→            return True, "Agent paused"
   330→        except psutil.NoSuchProcess:
   331→            self.status = "crashed"
   332→            self._remove_lock()
   333→            return False, "Agent process no longer exists"
   334→        except Exception as e:
   335→            logger.exception("Failed to pause agent")
   336→            return False, f"Failed to pause agent: {e}"
   337→
   338→    async def resume(self) -> tuple[bool, str]:
   339→        """
   340→        Resume a paused agent.
   341→
   342→        Returns:
   343→            Tuple of (success, message)
   344→        """
   345→        if not self.process or self.status != "paused":
   346→            return False, "Agent is not paused"
   347→
   348→        try:
   349→            proc = psutil.Process(self.process.pid)
   350→            proc.resume()
   351→            self.status = "running"
   352→            return True, "Agent resumed"
   353→        except psutil.NoSuchProcess:
   354→            self.status = "crashed"
   355→            self._remove_lock()
   356→            return False, "Agent process no longer exists"
   357→        except Exception as e:
   358→            logger.exception("Failed to resume agent")
   359→            return False, f"Failed to resume agent: {e}"
   360→
   361→    async def healthcheck(self) -> bool:
   362→        """
   363→        Check if the agent process is still alive.
   364→
   365→        Updates status to 'crashed' if process has died unexpectedly.
   366→
   367→        Returns:
   368→            True if healthy, False otherwise
   369→        """
   370→        if not self.process:
   371→            return self.status == "stopped"
   372→
   373→        poll = self.process.poll()
   374→        if poll is not None:
   375→            # Process has terminated
   376→            if self.status in ("running", "paused"):
   377→                self.status = "crashed"
   378→                self._remove_lock()
   379→            return False
   380→
   381→        return True
   382→
   383→    def get_status_dict(self) -> dict:
   384→        """Get current status as a dictionary."""
   385→        return {
   386→            "status": self.status,
   387→            "pid": self.pid,
   388→            "started_at": self.started_at.isoformat() if self.started_at else None,
   389→            "yolo_mode": self.yolo_mode,
   390→        }
   391→
   392→
   393→# Global registry of process managers per project with thread safety
   394→_managers: dict[str, AgentProcessManager] = {}
   395→_managers_lock = threading.Lock()
   396→
   397→
   398→def get_manager(project_name: str, project_dir: Path, root_dir: Path) -> AgentProcessManager:
   399→    """Get or create a process manager for a project (thread-safe).
   400→
   401→    Args:
   402→        project_name: Name of the project
   403→        project_dir: Absolute path to the project directory
   404→        root_dir: Root directory of the autonomous-coding-ui project
   405→    """
   406→    with _managers_lock:
   407→        if project_name not in _managers:
   408→            _managers[project_name] = AgentProcessManager(project_name, project_dir, root_dir)
   409→        return _managers[project_name]
   410→
   411→
   412→async def cleanup_all_managers() -> None:
   413→    """Stop all running agents. Called on server shutdown."""
   414→    with _managers_lock:
   415→        managers = list(_managers.values())
   416→
   417→    for manager in managers:
   418→        try:
   419→            if manager.status != "stopped":
   420→                await manager.stop()
   421→        except Exception as e:
   422→            logger.warning(f"Error stopping manager for {manager.project_name}: {e}")
   423→
   424→    with _managers_lock:
   425→        _managers.clear()
   426→

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

"""
Agent Process Manager
=====================

Manages the lifecycle of agent subprocesses per project.
Provides start/stop/pause/resume functionality with cross-platform support.
"""

import asyncio
import logging
import re
import subprocess
import sys
import threading
from datetime import datetime
from pathlib import Path
from typing import Awaitable, Callable, Literal, Set

import psutil

logger = logging.getLogger(__name__)

# Patterns for sensitive data that should be redacted from output
SENSITIVE_PATTERNS = [
    r'sk-[a-zA-Z0-9]{20,}',  # Anthropic API keys
    r'ANTHROPIC_API_KEY=[^\s]+',
    r'api[_-]?key[=:][^\s]+',
    r'token[=:][^\s]+',
    r'password[=:][^\s]+',
    r'secret[=:][^\s]+',
    r'ghp_[a-zA-Z0-9]{36,}',  # GitHub personal access tokens
    r'gho_[a-zA-Z0-9]{36,}',  # GitHub OAuth tokens
    r'ghs_[a-zA-Z0-9]{36,}',  # GitHub server tokens
    r'ghr_[a-zA-Z0-9]{36,}',  # GitHub refresh tokens
    r'aws[_-]?access[_-]?key[=:][^\s]+',  # AWS keys
    r'aws[_-]?secret[=:][^\s]+',
]

def sanitize_output(line: str) -> str:
    """Remove sensitive information from output lines."""
    for pattern in SENSITIVE_PATTERNS:
        line = re.sub(pattern, '[REDACTED]', line, flags=re.IGNORECASE)
    return line

class AgentProcessManager:
    """
    Manages agent subprocess lifecycle for a single project.

    Provides start/stop/pause/resume with cross-platform support via psutil.
    Supports multiple output callbacks for WebSocket clients.
    """

    def __init__(
        self,
        project_name: str,
        project_dir: Path,
        root_dir: Path,
    ):
        """
        Initialize the process manager.

        Args:
            project_name: Name of the project
            project_dir: Absolute path to the project directory
            root_dir: Root directory of the autonomous-coding-ui project
        """
        self.project_name = project_name
        self.project_dir = project_dir
        self.root_dir = root_dir
        self.process: subprocess.Popen | None = None
        self._status: Literal["stopped", "running", "paused", "crashed"] = "stopped"
        self.started_at: datetime | None = None
        self._output_task: asyncio.Task | None = None
        self.yolo_mode: bool = False  # YOLO mode for rapid prototyping

        # Support multiple callbacks (for multiple WebSocket clients)
        self._output_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
        self._status_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
        self._callbacks_lock = threading.Lock()

        # Lock file to prevent multiple instances (stored in project directory)
        self.lock_file = self.project_dir / ".agent.lock"

    @property
    def status(self) -> Literal["stopped", "running", "paused", "crashed"]:
        return self._status

    @status.setter
    def status(self, value: Literal["stopped", "running", "paused", "crashed"]):
        old_status = self._status
        self._status = value
        if old_status != value:
            self._notify_status_change(value)

    def _notify_status_change(self, status: str) -> None:
        """Notify all registered callbacks of status change."""
        with self._callbacks_lock:
            callbacks = list(self._status_callbacks)

        for callback in callbacks:
            try:
                # Schedule the callback in the event loop
                loop = asyncio.get_running_loop()
                loop.create_task(self._safe_callback(callback, status))
            except RuntimeError:
                # No running event loop
                pass

    async def _safe_callback(self, callback: Callable, *args) -> None:
        """Safely execute a callback, catching and logging any errors."""
        try:
            await callback(*args)
        except Exception as e:
            logger.warning(f"Callback error: {e}")

    def add_output_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
        """Add a callback for output lines."""
        with self._callbacks_lock:
            self._output_callbacks.add(callback)

    def remove_output_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
        """Remove an output callback."""
        with self._callbacks_lock:
            self._output_callbacks.discard(callback)

    def add_status_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
        """Add a callback for status changes."""
        with self._callbacks_lock:
            self._status_callbacks.add(callback)

    def remove_status_callback(self, callback: Callable[[str], Awaitable[None]]) -> None:
        """Remove a status callback."""
        with self._callbacks_lock:
            self._status_callbacks.discard(callback)

    @property
    def pid(self) -> int | None:
        return self.process.pid if self.process else None

    def _check_lock(self) -> bool:
        """Check if another agent is already running for this project."""
        if not self.lock_file.exists():
            return True

        try:
            pid = int(self.lock_file.read_text().strip())
            if psutil.pid_exists(pid):
                # Check if it's actually our agent process
                try:
                    proc = psutil.Process(pid)
                    cmdline = " ".join(proc.cmdline())
                    if "autonomous_agent_demo.py" in cmdline:
                        return False  # Another agent is running
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    pass
            # Stale lock file
            self.lock_file.unlink(missing_ok=True)
            return True
        except (ValueError, OSError):
            self.lock_file.unlink(missing_ok=True)
            return True

    def _create_lock(self) -> None:
        """Create lock file with current process PID."""
        self.lock_file.parent.mkdir(parents=True, exist_ok=True)
        if self.process:
            self.lock_file.write_text(str(self.process.pid))

    def _remove_lock(self) -> None:
        """Remove lock file."""
        self.lock_file.unlink(missing_ok=True)

    async def _broadcast_output(self, line: str) -> None:
        """Broadcast output line to all registered callbacks."""
        with self._callbacks_lock:
            callbacks = list(self._output_callbacks)

        for callback in callbacks:
            await self._safe_callback(callback, line)

    async def _stream_output(self) -> None:
        """Stream process output to callbacks."""
        if not self.process or not self.process.stdout:
            return

        try:
            loop = asyncio.get_running_loop()
            while True:
                # Use run_in_executor for blocking readline
                line = await loop.run_in_executor(
                    None, self.process.stdout.readline
                )
                if not line:
                    break

                decoded = line.decode("utf-8", errors="replace").rstrip()
                sanitized = sanitize_output(decoded)

                await self._broadcast_output(sanitized)

        except asyncio.CancelledError:
            raise
        except Exception as e:
            logger.warning(f"Output streaming error: {e}")
        finally:
            # Check if process ended
            if self.process and self.process.poll() is not None:
                exit_code = self.process.returncode
                if exit_code != 0 and self.status == "running":
                    self.status = "crashed"
                elif self.status == "running":
                    self.status = "stopped"
                self._remove_lock()

    async def start(self, yolo_mode: bool = False) -> tuple[bool, str]:
        """
        Start the agent as a subprocess.

        Args:
            yolo_mode: If True, run in YOLO mode (no browser testing)

        Returns:
            Tuple of (success, message)
        """
        if self.status in ("running", "paused"):
            return False, f"Agent is already {self.status}"

        if not self._check_lock():
            return False, "Another agent instance is already running for this project"

        # Store YOLO mode for status queries
        self.yolo_mode = yolo_mode

        # Build command - pass absolute path to project directory
        cmd = [
            sys.executable,
            str(self.root_dir / "autonomous_agent_demo.py"),
            "--project-dir",
            str(self.project_dir.resolve()),
        ]

        # Add --yolo flag if YOLO mode is enabled
        if yolo_mode:
            cmd.append("--yolo")

        try:
            # Start subprocess with piped stdout/stderr
            # Use project_dir as cwd so Claude SDK sandbox allows access to project files
            self.process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                cwd=str(self.project_dir),
            )

            self._create_lock()
            self.started_at = datetime.now()
            self.status = "running"

            # Start output streaming task
            self._output_task = asyncio.create_task(self._stream_output())

            return True, f"Agent started with PID {self.process.pid}"
        except Exception as e:
            logger.exception("Failed to start agent")
            return False, f"Failed to start agent: {e}"

    async def stop(self) -> tuple[bool, str]:
        """
        Stop the agent (SIGTERM then SIGKILL if needed).

        Returns:
            Tuple of (success, message)
        """
        if not self.process or self.status == "stopped":
            return False, "Agent is not running"

        try:
            # Cancel output streaming
            if self._output_task:
                self._output_task.cancel()
                try:
                    await self._output_task
                except asyncio.CancelledError:
                    pass

            # Terminate gracefully first
            self.process.terminate()

            # Wait up to 5 seconds for graceful shutdown
            loop = asyncio.get_running_loop()
            try:
                await asyncio.wait_for(
                    loop.run_in_executor(None, self.process.wait),
                    timeout=5.0
                )
            except asyncio.TimeoutError:
                # Force kill if still running
                self.process.kill()
                await loop.run_in_executor(None, self.process.wait)

            self._remove_lock()
            self.status = "stopped"
            self.process = None
            self.started_at = None
            self.yolo_mode = False  # Reset YOLO mode

            return True, "Agent stopped"
        except Exception as e:
            logger.exception("Failed to stop agent")
            return False, f"Failed to stop agent: {e}"

    async def pause(self) -> tuple[bool, str]:
        """
        Pause the agent using psutil for cross-platform support.

        Returns:
            Tuple of (success, message)
        """
        if not self.process or self.status != "running":
            return False, "Agent is not running"

        try:
            proc = psutil.Process(self.process.pid)
            proc.suspend()
            self.status = "paused"
            return True, "Agent paused"
        except psutil.NoSuchProcess:
            self.status = "crashed"
            self._remove_lock()
            return False, "Agent process no longer exists"
        except Exception as e:
            logger.exception("Failed to pause agent")
            return False, f"Failed to pause agent: {e}"

    async def resume(self) -> tuple[bool, str]:
        """
        Resume a paused agent.

        Returns:
            Tuple of (success, message)
        """
        if not self.process or self.status != "paused":
            return False, "Agent is not paused"

        try:
            proc = psutil.Process(self.process.pid)
            proc.resume()
            self.status = "running"
            return True, "Agent resumed"
        except psutil.NoSuchProcess:
            self.status = "crashed"
            self._remove_lock()
            return False, "Agent process no longer exists"
        except Exception as e:
            logger.exception("Failed to resume agent")
            return False, f"Failed to resume agent: {e}"

    async def healthcheck(self) -> bool:
        """
        Check if the agent process is still alive.

        Updates status to 'crashed' if process has died unexpectedly.

        Returns:
            True if healthy, False otherwise
        """
        if not self.process:
            return self.status == "stopped"

        poll = self.process.poll()
        if poll is not None:
            # Process has terminated
            if self.status in ("running", "paused"):
                self.status = "crashed"
                self._remove_lock()
            return False

        return True

    def get_status_dict(self) -> dict:
        """Get current status as a dictionary."""
        return {
            "status": self.status,
            "pid": self.pid,
            "started_at": self.started_at.isoformat() if self.started_at else None,
            "yolo_mode": self.yolo_mode,
        }

# Global registry of process managers per project with thread safety
_managers: dict[str, AgentProcessManager] = {}
_managers_lock = threading.Lock()

def get_manager(project_name: str, project_dir: Path, root_dir: Path) -> AgentProcessManager:
    """Get or create a process manager for a project (thread-safe).

    Args:
        project_name: Name of the project
        project_dir: Absolute path to the project directory
        root_dir: Root directory of the autonomous-coding-ui project
    """
    with _managers_lock:
        if project_name not in _managers:
            _managers[project_name] = AgentProcessManager(project_name, project_dir, root_dir)
        return _managers[project_name]

async def cleanup_all_managers() -> None:
    """Stop all running agents. Called on server shutdown."""
    with _managers_lock:
        managers = list(_managers.values())

    for manager in managers:
        try:
            if manager.status != "stopped":
                await manager.stop()
        except Exception as e:
            logger.warning(f"Error stopping manager for {manager.project_name}: {e}")

    with _managers_lock:
        _managers.clear()


1→import { useState, useEffect, useCallback } from 'react'
     2→import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
     3→import { useProjectWebSocket } from './hooks/useWebSocket'
     4→import { useFeatureSound } from './hooks/useFeatureSound'
     5→import { useCelebration } from './hooks/useCelebration'
     6→
     7→const STORAGE_KEY = 'autocoder-selected-project'
     8→import { ProjectSelector } from './components/ProjectSelector'
     9→import { KanbanBoard } from './components/KanbanBoard'
    10→import { AgentControl } from './components/AgentControl'
    11→import { ProgressDashboard } from './components/ProgressDashboard'
    12→import { SetupWizard } from './components/SetupWizard'
    13→import { AddFeatureForm } from './components/AddFeatureForm'
    14→import { FeatureModal } from './components/FeatureModal'
    15→import { DebugLogViewer } from './components/DebugLogViewer'
    16→import { AgentThought } from './components/AgentThought'
    17→import { AssistantFAB } from './components/AssistantFAB'
    18→import { AssistantPanel } from './components/AssistantPanel'
    19→import { Plus, Loader2 } from 'lucide-react'
    20→import type { Feature } from './lib/types'
    21→
    22→function App() {
    23→  // Initialize selected project from localStorage
    24→  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    25→    try {
    26→      return localStorage.getItem(STORAGE_KEY)
    27→    } catch {
    28→      return null
    29→    }
    30→  })
    31→  const [showAddFeature, setShowAddFeature] = useState(false)
    32→  const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
    33→  const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
    34→  const [debugOpen, setDebugOpen] = useState(false)
    35→  const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
    36→  const [assistantOpen, setAssistantOpen] = useState(false)
    37→
    38→  const { data: projects, isLoading: projectsLoading } = useProjects()
    39→  const { data: features } = useFeatures(selectedProject)
    40→  const { data: agentStatusData } = useAgentStatus(selectedProject)
    41→  const wsState = useProjectWebSocket(selectedProject)
    42→
    43→  // Play sounds when features move between columns
    44→  useFeatureSound(features)
    45→
    46→  // Celebrate when all features are complete
    47→  useCelebration(features, selectedProject)
    48→
    49→  // Persist selected project to localStorage
    50→  const handleSelectProject = useCallback((project: string | null) => {
    51→    setSelectedProject(project)
    52→    try {
    53→      if (project) {
    54→        localStorage.setItem(STORAGE_KEY, project)
    55→      } else {
    56→        localStorage.removeItem(STORAGE_KEY)
    57→      }
    58→    } catch {
    59→      // localStorage not available
    60→    }
    61→  }, [])
    62→
    63→  // Validate stored project exists (clear if project was deleted)
    64→  useEffect(() => {
    65→    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
    66→      handleSelectProject(null)
    67→    }
    68→  }, [selectedProject, projects, handleSelectProject])
    69→
    70→  // Keyboard shortcuts
    71→  useEffect(() => {
    72→    const handleKeyDown = (e: KeyboardEvent) => {
    73→      // Ignore if user is typing in an input
    74→      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
    75→        return
    76→      }
    77→
    78→      // D : Toggle debug window
    79→      if (e.key === 'd' || e.key === 'D') {
    80→        e.preventDefault()
    81→        setDebugOpen(prev => !prev)
    82→      }
    83→
    84→      // N : Add new feature (when project selected)
    85→      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
    86→        e.preventDefault()
    87→        setShowAddFeature(true)
    88→      }
    89→
    90→      // A : Toggle assistant panel (when project selected)
    91→      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
    92→        e.preventDefault()
    93→        setAssistantOpen(prev => !prev)
    94→      }
    95→
    96→      // Escape : Close modals
    97→      if (e.key === 'Escape') {
    98→        if (assistantOpen) {
    99→          setAssistantOpen(false)
   100→        } else if (showAddFeature) {
   101→          setShowAddFeature(false)
   102→        } else if (selectedFeature) {
   103→          setSelectedFeature(null)
   104→        } else if (debugOpen) {
   105→          setDebugOpen(false)
   106→        }
   107→      }
   108→    }
   109→
   110→    window.addEventListener('keydown', handleKeyDown)
   111→    return () => window.removeEventListener('keydown', handleKeyDown)
   112→  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen])
   113→
   114→  // Combine WebSocket progress with feature data
   115→  const progress = wsState.progress.total > 0 ? wsState.progress : {
   116→    passing: features?.done.length ?? 0,
   117→    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
   118→    percentage: 0,
   119→  }
   120→
   121→  if (progress.total > 0 && progress.percentage === 0) {
   122→    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
   123→  }
   124→
   125→  if (!setupComplete) {
   126→    return <SetupWizard onComplete={() => setSetupComplete(true)} />
   127→  }
   128→
   129→  return (
   130→

   131→      {/* Header */}
   132→
   133→

   134→

   135→            {/* Logo and Title */}
   136→

   137→              AutoCoder
   138→

   139→
   140→            {/* Controls */}
   141→

   142→              <ProjectSelector
   143→                projects={projects ?? []}
   144→                selectedProject={selectedProject}
   145→                onSelectProject={handleSelectProject}
   146→                isLoading={projectsLoading}
   147→              />
   148→
   149→              {selectedProject && (
   150→                <>
   151→                  <button
   152→                    onClick={() => setShowAddFeature(true)}
   153→                    className="neo-btn neo-btn-primary text-sm"
   154→                    title="Press N"
   155→                  >
   156→                    <Plus size={18} />
   157→                    Add Feature
   158→
   159→                      N
   160→
   161→                  </button>
   162→
   163→                  <AgentControl
   164→                    projectName={selectedProject}
   165→                    status={wsState.agentStatus}
   166→                    yoloMode={agentStatusData?.yolo_mode ?? false}
   167→                  />
   168→                </>
   169→              )}
   170→

   171→

   172→

   173→
   174→
   175→      {/* Main Content */}
   176→
   180→        {!selectedProject ? (
   181→

   182→

   183→              Welcome to AutoCoder
   184→

   185→

   186→              Select a project from the dropdown above or create a new one to get started.
   187→

   188→

   189→        ) : (
   190→

   191→            {/* Progress Dashboard */}
   192→            <ProgressDashboard
   193→              passing={progress.passing}
   194→              total={progress.total}
   195→              percentage={progress.percentage}
   196→              isConnected={wsState.isConnected}
   197→            />
   198→
   199→            {/* Agent Thought - shows latest agent narrative */}
   200→            <AgentThought
   201→              logs={wsState.logs}
   202→              agentStatus={wsState.agentStatus}
   203→            />
   204→
   205→            {/* Initializing Features State - show when agent is running but no features yet */}
   206→            {features &&
   207→             features.pending.length === 0 &&
   208→             features.in_progress.length === 0 &&
   209→             features.done.length === 0 &&
   210→             wsState.agentStatus === 'running' && (
   211→

   212→                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />
   213→

   214→                  Initializing Features...
   215→

   216→

   217→                  The agent is reading your spec and creating features. This may take a moment.
   218→

   219→

   220→            )}
   221→
   222→            {/* Kanban Board */}
   223→            <KanbanBoard
   224→              features={features}
   225→              onFeatureClick={setSelectedFeature}
   226→            />
   227→

   228→        )}
   229→
   230→
   231→      {/* Add Feature Modal */}
   232→      {showAddFeature && selectedProject && (
   233→        <AddFeatureForm
   234→          projectName={selectedProject}
   235→          onClose={() => setShowAddFeature(false)}
   236→        />
   237→      )}
   238→
   239→      {/* Feature Detail Modal */}
   240→      {selectedFeature && selectedProject && (
   241→        <FeatureModal
   242→          feature={selectedFeature}
   243→          projectName={selectedProject}
   244→          onClose={() => setSelectedFeature(null)}
   245→        />
   246→      )}
   247→
   248→      {/* Debug Log Viewer - fixed to bottom */}
   249→      {selectedProject && (
   250→        <DebugLogViewer
   251→          logs={wsState.logs}
   252→          isOpen={debugOpen}
   253→          onToggle={() => setDebugOpen(!debugOpen)}
   254→          onClear={wsState.clearLogs}
   255→          onHeightChange={setDebugPanelHeight}
   256→        />
   257→      )}
   258→
   259→      {/* Assistant FAB and Panel */}
   260→      {selectedProject && (
   261→        <>
   262→          <AssistantFAB
   263→            onClick={() => setAssistantOpen(!assistantOpen)}
   264→            isOpen={assistantOpen}
   265→          />
   266→          <AssistantPanel
   267→            projectName={selectedProject}
   268→            isOpen={assistantOpen}
   269→            onClose={() => setAssistantOpen(false)}
   270→          />
   271→        </>
   272→      )}
   273→

   274→  )
   275→}
   276→
   277→export default App
   278→

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

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { Plus, Loader2 } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  const [showAddFeature, setShowAddFeature] = useState(false)
  const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
  const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
  const [debugOpen, setDebugOpen] = useState(false)
  const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* Initializing Features State - show when agent is running but no features yet */}
            {features &&
             features.pending.length === 0 &&
             features.in_progress.length === 0 &&
             features.done.length === 0 &&
             wsState.agentStatus === 'running' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

  )
}

export default App


Now let me look at the Claude SDK integration and how the autonomous agent demo works:


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

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

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

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

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

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

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

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

import argparse
import asyncio
from pathlib import Path

from dotenv import load_dotenv

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

from agent import run_autonomous_agent
from registry import get_project_path

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

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

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

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

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

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

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

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

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

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

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

    return parser.parse_args()

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

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

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

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

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

if __name__ == "__main__":
    main()


     1→# AutoCoder
     2→
     3→[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)
     4→
     5→A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time.
     6→
     7→## Video Tutorial
     8→
     9→[![Watch the tutorial](https://img.youtube.com/vi/lGWFlpffWk4/hqdefault.jpg)](https://youtu.be/lGWFlpffWk4)
    10→
    11→> **[Watch the setup and usage guide →](https://youtu.be/lGWFlpffWk4)**
    12→
    13→---
    14→
    15→## Prerequisites
    16→
    17→### Claude Code CLI (Required)
    18→
    19→This project requires the Claude Code CLI to be installed. Install it using one of these methods:
    20→
    21→**macOS / Linux:**
    22→```bash
    23→curl -fsSL https://claude.ai/install.sh | bash
    24→```
    25→
    26→**Windows (PowerShell):**
    27→```powershell
    28→irm https://claude.ai/install.ps1 | iex
    29→```
    30→
    31→### Authentication
    32→
    33→You need one of the following:
    34→
    35→- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended)
    36→- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/
    37→
    38→---
    39→
    40→## Quick Start
    41→
    42→### Option 1: Web UI (Recommended)
    43→
    44→**Windows:**
    45→```cmd
    46→start_ui.bat
    47→```
    48→
    49→**macOS / Linux:**
    50→```bash
    51→./start_ui.sh
    52→```
    53→
    54→This launches the React-based web UI at `http://localhost:5173` with:
    55→- Project selection and creation
    56→- Kanban board view of features
    57→- Real-time agent output streaming
    58→- Start/pause/stop controls
    59→
    60→### Option 2: CLI Mode
    61→
    62→**Windows:**
    63→```cmd
    64→start.bat
    65→```
    66→
    67→**macOS / Linux:**
    68→```bash
    69→./start.sh
    70→```
    71→
    72→The start script will:
    73→1. Check if Claude CLI is installed
    74→2. Check if you're authenticated (prompt to run `claude login` if not)
    75→3. Create a Python virtual environment
    76→4. Install dependencies
    77→5. Launch the main menu
    78→
    79→### Creating or Continuing a Project
    80→
    81→You'll see options to:
    82→- **Create new project** - Start a fresh project with AI-assisted spec generation
    83→- **Continue existing project** - Resume work on a previous project
    84→
    85→For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help.
    86→
    87→---
    88→
    89→## How It Works
    90→
    91→### Two-Agent Pattern
    92→
    93→1. **Initializer Agent (First Session):** Reads your app specification, creates features in a SQLite database (`features.db`), sets up the project structure, and initializes git.
    94→
    95→2. **Coding Agent (Subsequent Sessions):** Picks up where the previous session left off, implements features one by one, and marks them as passing in the database.
    96→
    97→### Feature Management
    98→
    99→Features are stored in SQLite via SQLAlchemy and managed through an MCP server that exposes tools to the agent:
   100→- `feature_get_stats` - Progress statistics
   101→- `feature_get_next` - Get highest-priority pending feature
   102→- `feature_get_for_regression` - Random passing features for regression testing
   103→- `feature_mark_passing` - Mark feature complete
   104→- `feature_skip` - Move feature to end of queue
   105→- `feature_create_bulk` - Initialize all features (used by initializer)
   106→
   107→### Session Management
   108→
   109→- Each session runs with a fresh context window
   110→- Progress is persisted via SQLite database and git commits
   111→- The agent auto-continues between sessions (3 second delay)
   112→- Press `Ctrl+C` to pause; run the start script again to resume
   113→
   114→---
   115→
   116→## Important Timing Expectations
   117→
   118→> **Note: Building complete applications takes time!**
   119→
   120→- **First session (initialization):** The agent generates feature test cases. This takes several minutes and may appear to hang - this is normal.
   121→
   122→- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.
   123→
   124→- **Full app:** Building all features typically requires **many hours** of total runtime across multiple sessions.
   125→
   126→**Tip:** The feature count in the prompts determines scope. For faster demos, you can modify your app spec to target fewer features (e.g., 20-50 features for a quick demo).
   127→
   128→---
   129→
   130→## Project Structure
   131→
   132→```
   133→autonomous-coding/
   134→├── start.bat                 # Windows CLI start script
   135→├── start.sh                  # macOS/Linux CLI start script
   136→├── start_ui.bat              # Windows Web UI start script
   137→├── start_ui.sh               # macOS/Linux Web UI start script
   138→├── start.py                  # CLI menu and project management
   139→├── start_ui.py               # Web UI backend (FastAPI server launcher)
   140→├── autonomous_agent_demo.py  # Agent entry point
   141→├── agent.py                  # Agent session logic
   142→├── client.py                 # Claude SDK client configuration
   143→├── security.py               # Bash command allowlist and validation
   144→├── progress.py               # Progress tracking utilities
   145→├── prompts.py                # Prompt loading utilities
   146→├── api/
   147→│   └── database.py           # SQLAlchemy models (Feature table)
   148→├── mcp_server/
   149→│   └── feature_mcp.py        # MCP server for feature management tools
   150→├── server/
   151→│   ├── main.py               # FastAPI REST API server
   152→│   ├── websocket.py          # WebSocket handler for real-time updates
   153→│   ├── schemas.py            # Pydantic schemas
   154→│   ├── routers/              # API route handlers
   155→│   └── services/             # Business logic services
   156→├── ui/                       # React frontend
   157→│   ├── src/
   158→│   │   ├── App.tsx           # Main app component
   159→│   │   ├── hooks/            # React Query and WebSocket hooks
   160→│   │   └── lib/              # API client and types
   161→│   ├── package.json
   162→│   └── vite.config.ts
   163→├── .claude/
   164→│   ├── commands/
   165→│   │   └── create-spec.md    # /create-spec slash command
   166→│   ├── skills/               # Claude Code skills
   167→│   └── templates/            # Prompt templates
   168→├── generations/              # Generated projects go here
   169→├── requirements.txt          # Python dependencies
   170→└── .env                      # Optional configuration (N8N webhook)
   171→```
   172→
   173→---
   174→
   175→## Generated Project Structure
   176→
   177→After the agent runs, your project directory will contain:
   178→
   179→```
   180→generations/my_project/
   181→├── features.db               # SQLite database (feature test cases)
   182→├── prompts/
   183→│   ├── app_spec.txt          # Your app specification
   184→│   ├── initializer_prompt.md # First session prompt
   185→│   └── coding_prompt.md      # Continuation session prompt
   186→├── init.sh                   # Environment setup script
   187→├── claude-progress.txt       # Session progress notes
   188→└── [application files]       # Generated application code
   189→```
   190→
   191→---
   192→
   193→## Running the Generated Application
   194→
   195→After the agent completes (or pauses), you can run the generated application:
   196→
   197→```bash
   198→cd generations/my_project
   199→
   200→# Run the setup script created by the agent
   201→./init.sh
   202→
   203→# Or manually (typical for Node.js apps):
   204→npm install
   205→npm run dev
   206→```
   207→
   208→The application will typically be available at `http://localhost:3000` or similar.
   209→
   210→---
   211→
   212→## Security Model
   213→
   214→This project uses a defense-in-depth security approach (see `security.py` and `client.py`):
   215→
   216→1. **OS-level Sandbox:** Bash commands run in an isolated environment
   217→2. **Filesystem Restrictions:** File operations restricted to the project directory only
   218→3. **Bash Allowlist:** Only specific commands are permitted:
   219→   - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`
   220→   - Node.js: `npm`, `node`
   221→   - Version control: `git`
   222→   - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)
   223→
   224→Commands not in the allowlist are blocked by the security hook.
   225→
   226→---
   227→
   228→## Web UI Development
   229→
   230→The React UI is located in the `ui/` directory.
   231→
   232→### Development Mode
   233→
   234→```bash
   235→cd ui
   236→npm install
   237→npm run dev      # Development server with hot reload
   238→```
   239→
   240→### Building for Production
   241→
   242→```bash
   243→cd ui
   244→npm run build    # Builds to ui/dist/
   245→```
   246→
   247→**Note:** The `start_ui.bat`/`start_ui.sh` scripts serve the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` to see them when using the start scripts.
   248→
   249→### Tech Stack
   250→
   251→- React 18 with TypeScript
   252→- TanStack Query for data fetching
   253→- Tailwind CSS v4 with neobrutalism design
   254→- Radix UI components
   255→- WebSocket for real-time updates
   256→
   257→### Real-time Updates
   258→
   259→The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
   260→- `progress` - Test pass counts
   261→- `agent_status` - Running/paused/stopped/crashed
   262→- `log` - Agent output lines (streamed from subprocess stdout)
   263→- `feature_update` - Feature status changes
   264→
   265→---
   266→
   267→## Configuration (Optional)
   268→
   269→### N8N Webhook Integration
   270→
   271→The agent can send progress notifications to an N8N webhook. Create a `.env` file:
   272→
   273→```bash
   274→# Optional: N8N webhook for progress notifications
   275→PROGRESS_N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/your-webhook-id
   276→```
   277→
   278→When test progress increases, the agent sends:
   279→
   280→```json
   281→{
   282→  "event": "test_progress",
   283→  "passing": 45,
   284→  "total": 200,
   285→  "percentage": 22.5,
   286→  "project": "my_project",
   287→  "timestamp": "2025-01-15T14:30:00.000Z"
   288→}
   289→```
   290→
   291→---
   292→
   293→## Customization
   294→
   295→### Changing the Application
   296→
   297→Use the `/create-spec` command when creating a new project, or manually edit the files in your project's `prompts/` directory:
   298→- `app_spec.txt` - Your application specification
   299→- `initializer_prompt.md` - Controls feature generation
   300→
   301→### Modifying Allowed Commands
   302→
   303→Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.
   304→
   305→---
   306→
   307→## Troubleshooting
   308→
   309→**"Claude CLI not found"**
   310→Install the Claude Code CLI using the instructions in the Prerequisites section.
   311→
   312→**"Not authenticated with Claude"**
   313→Run `claude login` to authenticate. The start script will prompt you to do this automatically.
   314→
   315→**"Appears to hang on first run"**
   316→This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.
   317→
   318→**"Command blocked by security hook"**
   319→The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
   320→
   321→---
   322→
   323→## License
   324→
   325→This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details.
   326→Copyright (C) 2026 Leon van Zyl (https://leonvanzyl.com)
   327→

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

# AutoCoder

[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)

A long-running autonomous coding agent powered by the Claude Agent SDK. This tool can build complete applications over multiple sessions using a two-agent pattern (initializer + coding agent). Includes a React-based UI for monitoring progress in real-time.

## Video Tutorial

[![Watch the tutorial](https://img.youtube.com/vi/lGWFlpffWk4/hqdefault.jpg)](https://youtu.be/lGWFlpffWk4)

> **[Watch the setup and usage guide →](https://youtu.be/lGWFlpffWk4)**

---

## Prerequisites

### Claude Code CLI (Required)

This project requires the Claude Code CLI to be installed. Install it using one of these methods:

**macOS / Linux:**
```bash
curl -fsSL https://claude.ai/install.sh | bash
```

**Windows (PowerShell):**
```powershell
irm https://claude.ai/install.ps1 | iex
```

### Authentication

You need one of the following:

- **Claude Pro/Max Subscription** - Use `claude login` to authenticate (recommended)
- **Anthropic API Key** - Pay-per-use from https://console.anthropic.com/

---

## Quick Start

### Option 1: Web UI (Recommended)

**Windows:**
```cmd
start_ui.bat
```

**macOS / Linux:**
```bash
./start_ui.sh
```

This launches the React-based web UI at `http://localhost:5173` with:
- Project selection and creation
- Kanban board view of features
- Real-time agent output streaming
- Start/pause/stop controls

### Option 2: CLI Mode

**Windows:**
```cmd
start.bat
```

**macOS / Linux:**
```bash
./start.sh
```

The start script will:
1. Check if Claude CLI is installed
2. Check if you're authenticated (prompt to run `claude login` if not)
3. Create a Python virtual environment
4. Install dependencies
5. Launch the main menu

### Creating or Continuing a Project

You'll see options to:
- **Create new project** - Start a fresh project with AI-assisted spec generation
- **Continue existing project** - Resume work on a previous project

For new projects, you can use the built-in `/create-spec` command to interactively create your app specification with Claude's help.

---

## How It Works

### Two-Agent Pattern

1. **Initializer Agent (First Session):** Reads your app specification, creates features in a SQLite database (`features.db`), sets up the project structure, and initializes git.

2. **Coding Agent (Subsequent Sessions):** Picks up where the previous session left off, implements features one by one, and marks them as passing in the database.

### Feature Management

Features are stored in SQLite via SQLAlchemy and managed through an MCP server that exposes tools to the agent:
- `feature_get_stats` - Progress statistics
- `feature_get_next` - Get highest-priority pending feature
- `feature_get_for_regression` - Random passing features for regression testing
- `feature_mark_passing` - Mark feature complete
- `feature_skip` - Move feature to end of queue
- `feature_create_bulk` - Initialize all features (used by initializer)

### Session Management

- Each session runs with a fresh context window
- Progress is persisted via SQLite database and git commits
- The agent auto-continues between sessions (3 second delay)
- Press `Ctrl+C` to pause; run the start script again to resume

---

## Important Timing Expectations

> **Note: Building complete applications takes time!**

- **First session (initialization):** The agent generates feature test cases. This takes several minutes and may appear to hang - this is normal.

- **Subsequent sessions:** Each coding iteration can take **5-15 minutes** depending on complexity.

- **Full app:** Building all features typically requires **many hours** of total runtime across multiple sessions.

**Tip:** The feature count in the prompts determines scope. For faster demos, you can modify your app spec to target fewer features (e.g., 20-50 features for a quick demo).

---

## Project Structure

```
autonomous-coding/
├── start.bat                 # Windows CLI start script
├── start.sh                  # macOS/Linux CLI start script
├── start_ui.bat              # Windows Web UI start script
├── start_ui.sh               # macOS/Linux Web UI start script
├── start.py                  # CLI menu and project management
├── start_ui.py               # Web UI backend (FastAPI server launcher)
├── autonomous_agent_demo.py  # Agent entry point
├── agent.py                  # Agent session logic
├── client.py                 # Claude SDK client configuration
├── security.py               # Bash command allowlist and validation
├── progress.py               # Progress tracking utilities
├── prompts.py                # Prompt loading utilities
├── api/
│   └── database.py           # SQLAlchemy models (Feature table)
├── mcp_server/
│   └── feature_mcp.py        # MCP server for feature management tools
├── server/
│   ├── main.py               # FastAPI REST API server
│   ├── websocket.py          # WebSocket handler for real-time updates
│   ├── schemas.py            # Pydantic schemas
│   ├── routers/              # API route handlers
│   └── services/             # Business logic services
├── ui/                       # React frontend
│   ├── src/
│   │   ├── App.tsx           # Main app component
│   │   ├── hooks/            # React Query and WebSocket hooks
│   │   └── lib/              # API client and types
│   ├── package.json
│   └── vite.config.ts
├── .claude/
│   ├── commands/
│   │   └── create-spec.md    # /create-spec slash command
│   ├── skills/               # Claude Code skills
│   └── templates/            # Prompt templates
├── generations/              # Generated projects go here
├── requirements.txt          # Python dependencies
└── .env                      # Optional configuration (N8N webhook)
```

---

## Generated Project Structure

After the agent runs, your project directory will contain:

```
generations/my_project/
├── features.db               # SQLite database (feature test cases)
├── prompts/
│   ├── app_spec.txt          # Your app specification
│   ├── initializer_prompt.md # First session prompt
│   └── coding_prompt.md      # Continuation session prompt
├── init.sh                   # Environment setup script
├── claude-progress.txt       # Session progress notes
└── [application files]       # Generated application code
```

---

## Running the Generated Application

After the agent completes (or pauses), you can run the generated application:

```bash
cd generations/my_project

# Run the setup script created by the agent
./init.sh

# Or manually (typical for Node.js apps):
npm install
npm run dev
```

The application will typically be available at `http://localhost:3000` or similar.

---

## Security Model

This project uses a defense-in-depth security approach (see `security.py` and `client.py`):

1. **OS-level Sandbox:** Bash commands run in an isolated environment
2. **Filesystem Restrictions:** File operations restricted to the project directory only
3. **Bash Allowlist:** Only specific commands are permitted:
   - File inspection: `ls`, `cat`, `head`, `tail`, `wc`, `grep`
   - Node.js: `npm`, `node`
   - Version control: `git`
   - Process management: `ps`, `lsof`, `sleep`, `pkill` (dev processes only)

Commands not in the allowlist are blocked by the security hook.

---

## Web UI Development

The React UI is located in the `ui/` directory.

### Development Mode

```bash
cd ui
npm install
npm run dev      # Development server with hot reload
```

### Building for Production

```bash
cd ui
npm run build    # Builds to ui/dist/
```

**Note:** The `start_ui.bat`/`start_ui.sh` scripts serve the pre-built UI from `ui/dist/`. After making UI changes, run `npm run build` to see them when using the start scripts.

### Tech Stack

- React 18 with TypeScript
- TanStack Query for data fetching
- Tailwind CSS v4 with neobrutalism design
- Radix UI components
- WebSocket for real-time updates

### Real-time Updates

The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
- `progress` - Test pass counts
- `agent_status` - Running/paused/stopped/crashed
- `log` - Agent output lines (streamed from subprocess stdout)
- `feature_update` - Feature status changes

---

## Configuration (Optional)

### N8N Webhook Integration

The agent can send progress notifications to an N8N webhook. Create a `.env` file:

```bash
# Optional: N8N webhook for progress notifications
PROGRESS_N8N_WEBHOOK_URL=https://your-n8n-instance.com/webhook/your-webhook-id
```

When test progress increases, the agent sends:

```json
{
  "event": "test_progress",
  "passing": 45,
  "total": 200,
  "percentage": 22.5,
  "project": "my_project",
  "timestamp": "2025-01-15T14:30:00.000Z"
}
```

---

## Customization

### Changing the Application

Use the `/create-spec` command when creating a new project, or manually edit the files in your project's `prompts/` directory:
- `app_spec.txt` - Your application specification
- `initializer_prompt.md` - Controls feature generation

### Modifying Allowed Commands

Edit `security.py` to add or remove commands from `ALLOWED_COMMANDS`.

---

## Troubleshooting

**"Claude CLI not found"**
Install the Claude Code CLI using the instructions in the Prerequisites section.

**"Not authenticated with Claude"**
Run `claude login` to authenticate. The start script will prompt you to do this automatically.

**"Appears to hang on first run"**
This is normal. The initializer agent is generating detailed test cases, which takes significant time. Watch for `[Tool: ...]` output to confirm the agent is working.

**"Command blocked by security hook"**
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.

---

## License

This project is licensed under the GNU Affero General Public License v3.0 - see the [LICENSE.md](LICENSE.md) file for details.
Copyright (C) 2026 Leon van Zyl (https://leonvanzyl.com)


Now I have a complete picture of Autocoder's architecture. Let me summarize what I've learned and map your requirements to the codebase:

---

## Architecture Analysis Complete! 🎉

I now fully understand Autocoder. Here's how it works and where your features would fit:

### Current Architecture

| Component | File(s) | Purpose |
|-----------|---------|---------|
| **CLI Launcher** | `start.py` | Project selection, spec creation flow |
| **Agent Core** | `agent.py`, `autonomous_agent_demo.py` | Main agent loop, session management |
| **Claude SDK Client** | `client.py` | Creates ClaudeSDKClient with permissions, hooks, MCP servers |
| **Security** | `security.py` | Bash command allowlist (PreToolUse hook) |
| **Feature Database** | `mcp_server/feature_mcp.py`, `api/database.py` | SQLite-backed feature tracking via MCP tools |
| **Process Manager** | `server/services/process_manager.py` | Start/stop/pause agent subprocess |
| **WebSocket** | `server/websocket.py` | Real-time progress & log streaming |
| **React UI** | `ui/src/App.tsx` + components | Kanban board, agent controls, debug log viewer |

---

## Mapping Your Requirements to Implementation

### 1. **Permission Approval System** 🔐 (THE BIG ONE)

**Current State:** Permissions are pre-configured in `client.py`:
- `defaultMode: "acceptEdits"` auto-approves file edits
- `allow: [...]` pre-whitelists tools like WebSearch, WebFetch
- Security hook in `security.py` uses an allowlist for bash commands

**The Problem:** Claude Agent SDK doesn't expose a permission callback mechanism. When Claude requests permission for something NOT pre-approved, there's no way to prompt you and respond.

**Solution Options:**
1. **Interactive Permission Queue** - Create a new MCP tool or websocket channel for permission requests. The agent would pause, UI would show the request, you approve/deny, and the agent continues.
2. **Broader Pre-Approval** - Expand the permissions in `client.py` to include everything the agent might need
3. **Permission Config File** - Let you configure folder access, tool access in a project-level config that gets loaded at startup

### 2. **Spec Refinement / Re-validation** 🔄

**Current State:**
- Spec creation is one-time via `/create-spec`
- No mechanism to update spec and re-validate existing features

**Implementation:**
- Add "Edit Spec" flow in UI
- Add a feature re-validation tool that compares features against updated spec
- Mark features for re-testing if spec changes affect them

### 3. **Terminal Interface** 💻

**Current State:**
- `DebugLogViewer` shows agent output (read-only)
- No interactive terminal access

**Implementation:**
- Add a pseudo-terminal component in UI
- WebSocket channel for terminal I/O
- Use `pty` (Unix) or `winpty` (Windows) for true terminal emulation

### 4. **Infrastructure Awareness** 🏗

**Current State:**
- No mechanism to describe existing infrastructure

**Implementation:**
- Add `infrastructure.yaml` or `environment.md` to project prompts
- Include in agent's context (like app_spec.txt)
- Example: "You have access to: Nginx Proxy Manager at 192.168.1.x, PostgreSQL at db.local, etc."

### 5. **Parallel Agent Execution** ⚡

**Current State:**
- Single agent per project (`feature_mark_in_progress` prevents conflicts)
- Lock file prevents multiple instances

**Implementation:**
- Feature dependency graph to identify parallelizable work
- Multiple `AgentProcessManager` instances
- Smart feature assignment (avoid overlapping files/modules)
- Merge coordination for git commits

### 6. **Model Selection & Auto-Downgrade** 🧠

**Current State:**
- Model hardcoded in `autonomous_agent_demo.py` (`DEFAULT_MODEL = "claude-opus-4-5-20251101"`)
- YOLO mode exists but doesn't change model

**Implementation:**
- Add model selector to UI
- Store per-project model preference
- Agent can call an MCP tool to request model change based on task complexity
- Haiku for simple tasks, Sonnet for medium, Opus for complex

---

## Ready for Phase 2?

Now that I understand the codebase and your requirements, we can move forward with the spec creation process.

**Before I proceed, a few clarifying questions:**

1. **Permission System Priority:** Do you want a full interactive approval queue, or would a broader pre-configured permissions file suffice for now?

2. **Parallel Agents:** How many agents max? 2? 4? And should they share a single terminal view or have separate ones?

3. **Infrastructure Config:** What format would you prefer? YAML, Markdown, JSON? And what info would you typically include (IPs, ports, credentials, services)?

4. **Project Name:** You mentioned "remix" - do you have a name in mind for this fork? Something like "Autocoder Pro", "Autocoder+", or something entirely new?


Preconfig is fine, with option to edit the permissions list. 2 agents will be enough to complicate things, I'm sure.  IF that works, perhaps we add the ability ot use more.  Seperate terminals that I can interact with either if needed. No preference on config.  And Autocoder+ seems nice


Perfect! Let me capture all of this. We're ready to move into the formal spec creation process.

---

## Project Specification Summary

**Project Name:** Autocoder+

**Description:** A remix/fork of Autocoder that adds interactive permission management, spec refinement, parallel agent execution, terminal access, infrastructure awareness, and intelligent model selection.

**Target Audience:** Developers who want more control over autonomous coding agents - the ability to approve permissions, interact with terminals, leverage existing infrastructure, and run multiple agents in parallel.

---

Now let me ask about your involvement level:

**How involved do you want to be in technical decisions?**

1. **Quick Mode (Recommended)** - You've already described what you want clearly. I'll handle the database schema, API design, and architecture decisions based on your requirements.

2. **Detailed Mode** - You want input on specific technology choices, component architecture, and implementation details.

Which would you prefer?

And one more quick question:

**Technology preferences** - The original Autocoder uses:
- Python (FastAPI backend, Claude Agent SDK)
- React + TypeScript (UI)
- SQLite (feature database)
- WebSockets (real-time updates)

1. **Keep the same stack (Recommended)** - Build on top of what's already there
2. **I have changes** - Different preferences for some components


Keep the stack


Got it! Same stack, Quick Mode. Let me work through the features and derive everything.

---

## Feature Deep-Dive

Let me walk through each major feature area to make sure I capture everything:

### 1. Permission Management System

You mentioned pre-config with editable permissions list. Let me clarify a few things:

- **When would you edit permissions?** Before starting an agent, or also while it's running?
- **What kinds of permissions?** I'm thinking:
  - Folder access (read/write to specific paths outside project)
  - Tool access (WebSearch, WebFetch, Bash commands)
  - External service access (APIs, databases)
- **Should denied permissions block the agent or just skip that action?**

### 2. Parallel Agents with Separate Terminals

- **Two terminals side-by-side?** Or tabbed? Or something else?
- **When you "interact" with a terminal** - do you mean typing commands directly into the agent's environment, or pausing the agent and running your own commands?
- **How should agents coordinate?** I'm thinking:
  - Agent A claims features 1, 3, 5 (e.g., backend work)
  - Agent B claims features 2, 4, 6 (e.g., frontend work)
  - They avoid features that touch the same files

### 3. Spec Refinement & Re-validation

- **After updating the spec, what happens to existing features?**
  - Option A: Mark affected features for re-testing (passes → pending)
  - Option B: Show a diff of what changed, let you decide
  - Option C: Just update the spec, don't touch existing features
- **Can you edit individual features too?** Or just the main spec?

### 4. Infrastructure Awareness

You mentioned web server, Nginx Proxy Manager, build server. What else might you typically have?
- Database servers (PostgreSQL, MySQL, Redis)?
- Container orchestration (Docker, Kubernetes)?
- CI/CD pipelines?
- Cloud services (AWS, Azure, GCP)?
- Local dev tools (specific ports, services)?

This helps me design a flexible config format.

### 5. Model Selection

- **Per-project setting** that you can change anytime?
- **Per-agent setting** (so Agent A uses Opus, Agent B uses Haiku)?
- **Auto-downgrade triggers** - what would make the agent choose a simpler model? I'm thinking:
  - Simple file edits → Haiku
  - Standard feature implementation → Sonnet
  - Complex architecture decisions → Opus

---

Take your time on these - I want to get the details right before generating the spec!


1) If a task needs a certain permission which is not allowed, mark as stalled for that reason. 2) Interaction would be limited to granting a permission that it needs?  Or if that's not possible, jsut a place to see what's happening. 3) Option A 4) All of those things, perhaps that is part of the new spec creation/revisit, a chance for the agent to learn about where things should be, and test access to hose things


Excellent clarifications! Let me confirm my understanding and ask a couple final questions:

---

## Confirmed Understanding

### 1. Permission System
- Pre-configured permissions list (editable before/during agent run)
- If agent needs a permission it doesn't have → **mark feature as "stalled"** with reason
- You can then grant the permission and the feature goes back to pending

### 2. Terminals
- Two separate terminal views (one per agent)
- Primary purpose: **observe what's happening**
- Secondary: **grant permissions** when agent is stalled waiting for one
- Not full interactive shell (keeps it simpler)

### 3. Spec Refinement
- Can revisit/edit the spec anytime
- Affected features automatically marked for re-testing (passes → pending)

### 4. Infrastructure Discovery (Part of Spec Creation)
- During spec creation, agent asks about your infrastructure
- Agent tests connectivity to those resources
- Stores this in a config file the coding agents can reference
- Includes: databases, containers, CI/CD, cloud services, local dev tools, proxies, etc.

---

## Final Questions

### Model Selection
You didn't mention this one - still want it? Options:
1. **Yes, include it** - Model selector in UI, maybe auto-downgrade for simple tasks
2. **Skip for now** - Stick with one model, add this later

### Feature Scope
Based on everything discussed, here's what I'm counting as the major features for Autocoder+:

| Feature Area | Estimated Testable Features |
|--------------|----------------------------|
| Permission Management (pre-config, edit, stall/resume) | ~25 |
| Dual Agent Execution (parallel, coordination, separate views) | ~30 |
| Dual Terminal Views (observe, permission grants) | ~20 |
| Spec Refinement (edit spec, re-validate, mark for retest) | ~20 |
| Infrastructure Discovery (during spec, connectivity tests) | ~25 |
| Model Selection (if included) | ~15 |
| Core Autocoder Functionality (existing, adapted) | ~40 |
| **Total** | **~175** (or ~160 without model selection) |

Does this scope feel right? Too big? Too small? I can adjust - this is a **significant** project but doable in phases.

### One More Thing
Should the UI be an evolution of the existing Autocoder UI, or are you open to a fresh design? The current one uses a "neobrutalism" style (bold borders, chunky buttons). Keep that aesthetic or go for something different?


Skip model selection for now.  Make the UI sleek and modern.


Perfect! Let me tally up the final feature count and create your specification.

---

## Final Feature Breakdown

| Feature Area | Estimated Features |
|--------------|-------------------|
| Permission Management (pre-config, edit, stall/resume) | ~25 |
| Dual Agent Execution (parallel, coordination) | ~30 |
| Dual Terminal Views (observe, permission grants) | ~20 |
| Spec Refinement (edit spec, re-validate, mark for retest) | ~20 |
| Infrastructure Discovery (during spec, connectivity tests) | ~25 |
| Core Autocoder Functionality (adapted from original) | ~40 |
| **Total** | **~160 features** |

This is a substantial project - a full-featured enhancement of Autocoder. Does **160 features** feel right, or would you like me to adjust?

---

## Summary Before I Generate

**Project:** Autocoder+

**What it does:** A fork of Autocoder with:
- ✅ Editable permission system (features stall if permission missing)
- ✅ Dual parallel agents with coordination
- ✅ Two terminal views for observing agents & granting permissions
- ✅ Spec refinement with automatic re-validation
- ✅ Infrastructure discovery during spec creation
- ✅ Sleek, modern UI (replacing neobrutalism style)

**Tech Stack:** Python/FastAPI backend, React/TypeScript frontend, SQLite, WebSockets (same as original)

**Feature Count:** ~160

---

Ready to generate the specification files?

1. **Yes, generate files** - Create `app_spec.txt` and update prompt files
2. **I have changes** - Let me add or modify something first