Skip to content
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ This launches the React-based web UI at `http://localhost:5173` with:
- Kanban board view of features
- Real-time agent output streaming
- Start/pause/stop controls
- **Project Assistant** - AI chat for managing features and exploring the codebase

### Option 2: CLI Mode

Expand Down Expand Up @@ -103,6 +104,22 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
- `feature_mark_passing` - Mark feature complete
- `feature_skip` - Move feature to end of queue
- `feature_create_bulk` - Initialize all features (used by initializer)
- `feature_create` - Create a single feature
- `feature_update` - Update a feature's fields
- `feature_delete` - Delete a feature from the backlog

### Project Assistant

The Web UI includes a **Project Assistant** - an AI-powered chat interface for each project. Click the chat button in the bottom-right corner to open it.

**Capabilities:**
- **Explore the codebase** - Ask questions about files, architecture, and implementation details
- **Manage features** - Create, edit, delete, and deprioritize features via natural language
- **Get feature details** - Ask about specific features, their status, and test steps

**Conversation Persistence:**
- Conversations are automatically saved to `assistant.db` in the registered project directory
- When you navigate away and return, your conversation resumes where you left off

### Session Management

Expand Down Expand Up @@ -143,6 +160,7 @@ autonomous-coding/
├── security.py # Bash command allowlist and validation
├── progress.py # Progress tracking utilities
├── prompts.py # Prompt loading utilities
├── registry.py # Project registry (maps names to paths)
├── api/
│ └── database.py # SQLAlchemy models (Feature table)
├── mcp_server/
Expand All @@ -165,20 +183,25 @@ autonomous-coding/
│ │ └── create-spec.md # /create-spec slash command
│ ├── skills/ # Claude Code skills
│ └── templates/ # Prompt templates
├── generations/ # Generated projects go here
├── generations/ # Default location for new projects (can be anywhere)
├── requirements.txt # Python dependencies
└── .env # Optional configuration (N8N webhook)
```

---

## Generated Project Structure
## Project Registry and Structure

After the agent runs, your project directory will contain:
Projects can be stored in any directory on your filesystem. The **project registry** (`registry.py`) maps project names to their paths, stored in `~/.autocoder/registry.db` (SQLite).

```
generations/my_project/
When you create or register a project, the registry tracks its location. This allows projects to live anywhere - in `generations/`, your home directory, or any other path.

Each registered project directory will contain:

```text
<registered_project_path>/
├── features.db # SQLite database (feature test cases)
├── assistant.db # SQLite database (assistant chat history)
├── prompts/
│ ├── app_spec.txt # Your app specification
│ ├── initializer_prompt.md # First session prompt
Expand All @@ -192,10 +215,10 @@ generations/my_project/

## Running the Generated Application

After the agent completes (or pauses), you can run the generated application:
After the agent completes (or pauses), you can run the generated application. Navigate to your project's registered path (the directory you selected or created when setting up the project):

```bash
cd generations/my_project
cd /path/to/your/registered/project

# Run the setup script created by the agent
./init.sh
Expand Down
115 changes: 115 additions & 0 deletions mcp_server/feature_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- feature_clear_in_progress: Clear in-progress status
- feature_create_bulk: Create multiple features at once
- feature_create: Create a single feature
- feature_update: Update a feature's editable fields
"""

import json
Expand Down Expand Up @@ -477,5 +478,119 @@ def feature_create(
session.close()


@mcp.tool()
def feature_update(
feature_id: Annotated[int, Field(description="The ID of the feature to update", ge=1)],
category: Annotated[str | None, Field(default=None, min_length=1, max_length=100, description="New category (optional)")] = None,
name: Annotated[str | None, Field(default=None, min_length=1, max_length=255, description="New name (optional)")] = None,
description: Annotated[str | None, Field(default=None, min_length=1, description="New description (optional)")] = None,
steps: Annotated[list[str] | None, Field(default=None, min_length=1, description="New steps list (optional)")] = None,
) -> str:
"""Update an existing feature's editable fields.

Use this when the user asks to modify, update, edit, or change a feature.
Only the provided fields will be updated; others remain unchanged.

Cannot update: id, priority (use feature_skip), passes, in_progress (agent-controlled)

Args:
feature_id: The ID of the feature to update
category: New category (optional)
name: New name (optional)
description: New description (optional)
steps: New steps list (optional)

Returns:
JSON with the updated feature details, or error if not found.
"""
session = get_session()
try:
feature = session.query(Feature).filter(Feature.id == feature_id).first()

if feature is None:
return json.dumps({"error": f"Feature with ID {feature_id} not found"})

# Collect updates
updates = {}
if category is not None:
updates["category"] = category
if name is not None:
updates["name"] = name
if description is not None:
updates["description"] = description
if steps is not None:
updates["steps"] = steps

if not updates:
return json.dumps({"error": "No fields to update. Provide at least one of: category, name, description, steps"})

# Apply updates
for field, value in updates.items():
setattr(feature, field, value)

session.commit()
session.refresh(feature)

return json.dumps({
"success": True,
"message": f"Updated feature: {feature.name}",
"feature": feature.to_dict()
}, indent=2)
except Exception as e:
session.rollback()
return json.dumps({"error": str(e)})
finally:
session.close()


@mcp.tool()
def feature_delete(
feature_id: Annotated[int, Field(description="The ID of the feature to delete", ge=1)]
) -> str:
"""Delete a feature from the backlog.

Use this when the user asks to remove, delete, or drop a feature.
This removes the feature from tracking only - any implemented code remains.

For completed features, consider suggesting the user create a new "removal"
feature if they also want the code removed.

Args:
feature_id: The ID of the feature to delete

Returns:
JSON with success message and deleted feature details, or error if not found.
"""
session = get_session()
try:
feature = session.query(Feature).filter(Feature.id == feature_id).first()

if feature is None:
return json.dumps({"error": f"Feature with ID {feature_id} not found"})

feature_info = feature.to_dict()
was_passing = feature.passes

session.delete(feature)
session.commit()

result = {
"success": True,
"message": f"Deleted feature: {feature_info['name']}",
"deleted_feature": feature_info,
}

# Add hint for completed features
if was_passing:
result["note"] = "This feature was completed. The implemented code remains in the codebase. If you want the code removed, create a new feature describing what to remove."

return json.dumps(result, indent=2)
except Exception as e:
session.rollback()
return json.dumps({"error": str(e)})
finally:
session.close()


if __name__ == "__main__":
mcp.run()
48 changes: 48 additions & 0 deletions server/routers/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
FeatureCreate,
FeatureListResponse,
FeatureResponse,
FeatureUpdate,
)
from ..utils.validation import validate_project_name

Expand Down Expand Up @@ -289,6 +290,53 @@ async def skip_feature(project_name: str, feature_id: int):
raise HTTPException(status_code=500, detail="Failed to skip feature")


@router.patch("/{feature_id}", response_model=FeatureResponse)
async def update_feature(project_name: str, feature_id: int, update: FeatureUpdate):
"""
Update a feature's editable fields (category, name, description, steps).

Only provided fields are updated; others remain unchanged.
Cannot update: id, priority (use skip), passes, in_progress (agent-controlled).
"""
project_name = validate_project_name(project_name)
project_dir = _get_project_path(project_name)

if not project_dir:
raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found in registry")

if not project_dir.exists():
raise HTTPException(status_code=404, detail="Project directory not found")

_, Feature = _get_db_classes()

try:
with get_db_session(project_dir) as session:
feature = session.query(Feature).filter(Feature.id == feature_id).first()

if not feature:
raise HTTPException(status_code=404, detail=f"Feature {feature_id} not found")

# Get only the fields that were provided (exclude unset)
update_data = update.model_dump(exclude_unset=True)

if not update_data:
raise HTTPException(status_code=400, detail="No fields to update")

# Apply updates
for field, value in update_data.items():
setattr(feature, field, value)

session.commit()
session.refresh(feature)

return feature_to_response(feature)
except HTTPException:
raise
except Exception:
logger.exception("Failed to update feature")
raise HTTPException(status_code=500, detail="Failed to update feature")


@router.post("/bulk", response_model=FeatureBulkCreateResponse)
async def create_features_bulk(project_name: str, bulk: FeatureBulkCreate):
"""
Expand Down
8 changes: 8 additions & 0 deletions server/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class FeatureCreate(FeatureBase):
priority: int | None = None


class FeatureUpdate(BaseModel):
"""Request schema for updating a feature. All fields optional for partial updates."""
category: str | None = Field(None, min_length=1, max_length=100)
name: str | None = Field(None, min_length=1, max_length=255)
description: str | None = Field(None, min_length=1)
steps: list[str] | None = Field(None, min_length=1)


class FeatureResponse(FeatureBase):
"""Response schema for a feature."""
id: int
Expand Down
36 changes: 34 additions & 2 deletions server/services/assistant_chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ def get_cli_command() -> str:
"mcp__features__feature_get_for_regression",
]

# Feature management tools (create/skip but not mark_passing)
# Feature management tools (create/skip/update/delete but not mark_passing)
FEATURE_MANAGEMENT_TOOLS = [
"mcp__features__feature_create",
"mcp__features__feature_create_bulk",
"mcp__features__feature_skip",
"mcp__features__feature_update",
"mcp__features__feature_delete",
]

# Combined list for assistant
Expand Down Expand Up @@ -99,7 +101,9 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:

**Feature Management:**
- Create new features/test cases in the backlog
- Update existing features (name, description, category, steps)
- Skip features to deprioritize them (move to end of queue)
- Delete features from the backlog (removes tracking only, code remains)
- View feature statistics and progress

## What You CANNOT Do
Expand Down Expand Up @@ -129,6 +133,8 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
- **feature_create**: Create a single feature in the backlog
- **feature_create_bulk**: Create multiple features at once
- **feature_skip**: Move a feature to the end of the queue
- **feature_update**: Update a feature's category, name, description, or steps
- **feature_delete**: Remove a feature from the backlog (code remains)

## Creating Features

Expand All @@ -146,13 +152,39 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
[calls feature_create with appropriate parameters]
You: Done! I've added "S3 Sync Integration" to your backlog. It's now visible on the kanban board.

## Updating Features

When a user asks to update, modify, edit, or change a feature, use `feature_update`.
You can update any combination of: category, name, description, steps.
Only the fields you provide will be changed; others remain as-is.

**Example interaction:**
User: "Update feature 25 to have a better description"
You: I'll update that feature's description. What should the new description be?
User: "It should be 'Implement OAuth2 authentication with Google and GitHub providers'"
You: [calls feature_update with feature_id=25 and new description]
You: Done! I've updated the description for feature 25.

## Deleting Features

When a user asks to remove, delete, or drop a feature, use `feature_delete`.
This removes the feature from backlog tracking only - any implemented code remains in the codebase.

**Important:** For completed features, after deleting, suggest creating a new "removal" feature
if the user also wants the code removed. Example:
User: "Delete feature 123 and remove the implementation"
You: [calls feature_delete with feature_id=123]
You: Done! I've removed feature 123 from the backlog. Since this feature was already implemented,
the code still exists. Would you like me to create a new feature for the coding agent to remove
that implementation?

## Guidelines

1. Be concise and helpful
2. When explaining code, reference specific file paths and line numbers
3. Use the feature tools to answer questions about project progress
4. Search the codebase to find relevant information before answering
5. When creating features, confirm what was created
5. When creating or updating features, confirm what was done
6. If you're unsure about details, ask for clarification"""


Expand Down
Loading