Skip to content
Closed
13 changes: 8 additions & 5 deletions .claude/templates/coding_prompt.template.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -371,19 +371,22 @@ feature_get_stats
# 2. Get the NEXT feature to work on (one feature only)
feature_get_next

# 3. Mark a feature as in-progress (call immediately after feature_get_next)
# 3. Get a specific feature by ID (for parallel mode where feature is pre-assigned)
feature_get_by_id with feature_id={id}

# 4. Mark a feature as in-progress (call immediately after feature_get_next)
feature_mark_in_progress with feature_id={id}

# 4. Get up to 3 random passing features for regression testing
# 5. Get up to 3 random passing features for regression testing
feature_get_for_regression

# 5. Mark a feature as passing (after verification)
# 6. Mark a feature as passing (after verification)
feature_mark_passing with feature_id={id}

# 6. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
# 7. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
feature_skip with feature_id={id}

# 7. Clear in-progress status (when abandoning a feature)
# 8. Clear in-progress status (when abandoning a feature)
feature_clear_in_progress with feature_id={id}
```

Expand Down
11 changes: 7 additions & 4 deletions .claude/templates/coding_prompt_yolo.template.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,16 +205,19 @@ feature_get_stats
# 2. Get the NEXT feature to work on (one feature only)
feature_get_next

# 3. Mark a feature as in-progress (call immediately after feature_get_next)
# 3. Get a specific feature by ID (for parallel mode where feature is pre-assigned)
feature_get_by_id with feature_id={id}

# 4. Mark a feature as in-progress (call immediately after feature_get_next)
feature_mark_in_progress with feature_id={id}

# 4. Mark a feature as passing (after lint/type-check succeeds)
# 5. Mark a feature as passing (after lint/type-check succeeds)
feature_mark_passing with feature_id={id}

# 5. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
# 6. Skip a feature (moves to end of queue) - ONLY when blocked by dependency
feature_skip with feature_id={id}

# 6. Clear in-progress status (when abandoning a feature)
# 7. Clear in-progress status (when abandoning a feature)
feature_clear_in_progress with feature_id={id}
```

Expand Down
30 changes: 25 additions & 5 deletions agent.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,8 +25,8 @@
get_initializer_prompt,
get_coding_prompt,
get_coding_prompt_yolo,
get_coding_prompt_parallel,
copy_spec_to_project,
has_project_prompts,
)


Expand DownExpand Up@@ -113,20 +113,35 @@ async def run_autonomous_agent(
model: str,
max_iterations: Optional[int] = None,
yolo_mode: bool = False,
work_dir: Optional[Path] = None,
feature_id: Optional[int] = None,
) -> None:
"""
Run the autonomous agent loop.

Args:
project_dir: Directory for the project
project_dir: Directory for prompts and features.db
model: Claude model to use
max_iterations: Maximum number of iterations (None for unlimited)
yolo_mode: If True, skip browser testing and use YOLO prompt
work_dir: Directory for code changes (default: same as project_dir)
In parallel mode, this is the git worktree path.
feature_id: If provided, work on this specific feature (parallel mode).
Worker is bound to this feature and won't use feature_get_next.
"""
# work_dir defaults to project_dir if not specified
if work_dir is None:
work_dir = project_dir

is_parallel_mode = feature_id is not None

print("\n" + "=" * 70)
print(" AUTONOMOUS CODING AGENT DEMO")
print("=" * 70)
print(f"\nProject directory: {project_dir}")
if is_parallel_mode:
print(f"Worktree path: {work_dir}")
print(f"Bound to feature ID: {feature_id}")
print(f"Model: {model}")
if yolo_mode:
print("Mode: YOLO (testing disabled)")
Expand All@@ -138,8 +153,9 @@ async def run_autonomous_agent(
print("Max iterations: Unlimited (will run until completion)")
print()

# Create project directory
# Create directories
project_dir.mkdir(parents=True, exist_ok=True)
work_dir.mkdir(parents=True, exist_ok=True)

# Check if this is a fresh start or continuation
# Uses has_features() which checks if the database actually has features,
Expand DownExpand Up@@ -177,15 +193,19 @@ async def run_autonomous_agent(
print_session_header(iteration, is_first_run)

# Create client (fresh context)
client = create_client(project_dir, model, yolo_mode=yolo_mode)
# In parallel mode, work_dir is the worktree; project_dir has DB/prompts
client = create_client(project_dir, model, yolo_mode=yolo_mode, work_dir=work_dir)

# Choose prompt based on session type
# Pass project_dir to enable project-specific prompts
if is_first_run:
prompt = get_initializer_prompt(project_dir)
is_first_run = False # Only use initializer once
elif is_parallel_mode:
# Parallel mode: use feature_id-bound prompt
prompt = get_coding_prompt_parallel(project_dir, feature_id, yolo_mode)
else:
# Use YOLO prompt if in YOLO mode
# Single agent mode: use YOLO or standard prompt
if yolo_mode:
prompt = get_coding_prompt_yolo(project_dir)
else:
Expand Down
76 changes: 68 additions & 8 deletions api/database.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,17 +5,28 @@
SQLite database schema for feature storage using SQLAlchemy.
"""

import enum
from pathlib import Path
from typing import Optional

from sqlalchemy import Boolean, Column, Integer, String, Text, create_engine
from sqlalchemy import Boolean, Column, DateTime, Enum, Integer, String, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.types import JSON

Base = declarative_base()


class FeatureStatus(enum.Enum):
"""Status of a feature in the queue."""

PENDING = "pending" # Available for claiming
IN_PROGRESS = "in_progress" # Claimed by a worker (leased)
PASSING = "passing" # Completed successfully
CONFLICT = "conflict" # Merge conflict; manual resolution needed
FAILED = "failed" # Agent could not complete (permanent)


class Feature(Base):
"""Feature model representing a test case/feature to implement."""

Expand All@@ -27,9 +38,26 @@ class Feature(Base):
name = Column(String(255), nullable=False)
description = Column(Text, nullable=False)
steps = Column(JSON, nullable=False) # Stored as JSON array

# Status tracking (new enum-based status for parallel execution)
status = Column(
Enum(FeatureStatus, values_callable=lambda x: [e.value for e in x]),
default=FeatureStatus.PENDING,
index=True,
)

# Legacy fields - kept for backward compatibility
passes = Column(Boolean, default=False, index=True)
in_progress = Column(Boolean, default=False, index=True)

# Claim/lease tracking for parallel execution
claimed_by = Column(String(100), nullable=True) # Worker ID holding this feature
claimed_at = Column(DateTime, nullable=True) # Timestamp for lease expiry detection

# Completion audit
completed_at = Column(DateTime, nullable=True)
completed_by = Column(String(100), nullable=True)

def to_dict(self) -> dict:
"""Convert feature to dictionary for JSON serialization."""
return {
Expand All@@ -41,6 +69,11 @@ def to_dict(self) -> dict:
"steps": self.steps,
"passes": self.passes,
"in_progress": self.in_progress,
"status": self.status.value if self.status else FeatureStatus.PENDING.value,
"claimed_by": self.claimed_by,
"claimed_at": self.claimed_at.isoformat() if self.claimed_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"completed_by": self.completed_by,
}


Expand All@@ -58,19 +91,46 @@ def get_database_url(project_dir: Path) -> str:
return f"sqlite:///{db_path.as_posix()}"


def _migrate_add_in_progress_column(engine) -> None:
"""Add in_progress column to existing databases that don't have it."""
def _migrate_database_schema(engine) -> None:
"""Migrate existing databases to add new columns for parallel execution."""
from sqlalchemy import text

with engine.connect() as conn:
# Check if column exists
# Check existing columns
result = conn.execute(text("PRAGMA table_info(features)"))
columns = [row[1] for row in result.fetchall()]

# Migration: in_progress column (legacy)
if "in_progress" not in columns:
# Add the column with default value
conn.execute(text("ALTER TABLE features ADD COLUMN in_progress BOOLEAN DEFAULT 0"))
conn.commit()

# Migration: status column for parallel execution
if "status" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN status TEXT DEFAULT 'pending'"))
# Migrate existing data: passes=True -> 'passing', in_progress=True -> 'in_progress'
conn.execute(text("""
UPDATE features SET status = CASE
WHEN passes = 1 THEN 'passing'
WHEN in_progress = 1 THEN 'in_progress'
ELSE 'pending'
END
"""))

# Migration: claim tracking columns
if "claimed_by" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN claimed_by TEXT"))

if "claimed_at" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN claimed_at DATETIME"))

# Migration: completion audit columns
if "completed_at" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN completed_at DATETIME"))

if "completed_by" not in columns:
conn.execute(text("ALTER TABLE features ADD COLUMN completed_by TEXT"))

conn.commit()


def create_database(project_dir: Path) -> tuple:
Expand All@@ -87,8 +147,8 @@ def create_database(project_dir: Path) -> tuple:
engine = create_engine(db_url, connect_args={"check_same_thread": False})
Base.metadata.create_all(bind=engine)

# Migrate existing databases to add in_progress column
_migrate_add_in_progress_column(engine)
# Migrate existing databases to add new columns
_migrate_database_schema(engine)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
return engine, SessionLocal
Expand Down
23 changes: 21 additions & 2 deletions autonomous_agent_demo.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,6 @@

import argparse
import asyncio
import os
from pathlib import Path

from dotenv import load_dotenv
Expand DownExpand Up@@ -97,6 +96,21 @@ def parse_args() -> argparse.Namespace:
help="Enable YOLO mode: rapid prototyping without browser testing",
)

# Parallel execution arguments (used by parallel_coordinator.py)
parser.add_argument(
"--worktree-path",
type=str,
default=None,
help="Git worktree path for code changes (parallel mode only)",
)

parser.add_argument(
"--feature-id",
type=int,
default=None,
help="Feature ID to implement (parallel mode only - binds to claimed feature)",
)

return parser.parse_args()


Expand DownExpand Up@@ -128,14 +142,19 @@ def main() -> None:
print("Use an absolute path or register the project first.")
return

# In parallel mode, use worktree for code changes
work_dir = Path(args.worktree_path) if args.worktree_path else project_dir

try:
# Run the agent (MCP server handles feature database)
asyncio.run(
run_autonomous_agent(
project_dir=project_dir,
project_dir=project_dir, # For DB/prompts
work_dir=work_dir, # For code changes (may be worktree)
model=args.model,
max_iterations=args.max_iterations,
yolo_mode=args.yolo,
feature_id=args.feature_id, # Bound to claimed feature (parallel mode)
)
)
except KeyboardInterrupt:
Expand Down
Loading