Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .claude/templates/coding_prompt.template.md
Original file line number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import argparse
import asyncio
import os
from pathlib import Path

from dotenv import load_dotenv
Expand Down Expand 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 Down Expand 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