mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-22 13:23:08 +00:00
Compare commits
9 Commits
d65fa0ca56
...
472064c3da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
472064c3da | ||
|
|
afc2f4ac3c | ||
|
|
dceb535ade | ||
|
|
4f102e7bc2 | ||
|
|
9af0f309b7 | ||
|
|
49442f0d43 | ||
|
|
f786879908 | ||
|
|
dcdd06e02e | ||
|
|
b7aef15c3b |
@@ -55,10 +55,10 @@ Pull request(s): $ARGUMENTS
|
|||||||
- Reviewing large, unfocused PRs is impractical and error-prone; the review cannot provide adequate assurance for such changes
|
- Reviewing large, unfocused PRs is impractical and error-prone; the review cannot provide adequate assurance for such changes
|
||||||
|
|
||||||
6. **Vision Alignment Check**
|
6. **Vision Alignment Check**
|
||||||
- Read the project's README.md and CLAUDE.md to understand the application's core purpose
|
- **VISION.md protection**: First, check whether the PR diff modifies `VISION.md` in any way (edits, deletions, renames). If it does, **stop the review immediately** — verdict is **DON'T MERGE**. VISION.md is immutable and no PR is permitted to alter it. Explain this to the user and skip all remaining steps.
|
||||||
- Assess whether this PR aligns with the application's intended functionality
|
- Read the project's `VISION.md`, `README.md`, and `CLAUDE.md` to understand the application's core purpose and mandatory architectural constraints
|
||||||
- If the changes deviate significantly from the core vision or add functionality that doesn't serve the application's purpose, note this in the review
|
- Assess whether this PR aligns with the vision defined in `VISION.md`
|
||||||
- This is not a blocker, but should be flagged for the reviewer's consideration
|
- **Vision deviation is a merge blocker.** If the PR introduces functionality, integrations, or architectural changes that conflict with `VISION.md`, the verdict must be **DON'T MERGE**. This is not negotiable — the vision document takes precedence over any PR rationale.
|
||||||
|
|
||||||
7. **Safety Assessment**
|
7. **Safety Assessment**
|
||||||
- Provide a review on whether the PR is safe to merge as-is
|
- Provide a review on whether the PR is safe to merge as-is
|
||||||
|
|||||||
18
.claude/launch.json
Normal file
18
.claude/launch.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"runtimeExecutable": "python",
|
||||||
|
"runtimeArgs": ["-m", "uvicorn", "server.main:app", "--host", "127.0.0.1", "--port", "8888", "--reload"],
|
||||||
|
"port": 8888
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"runtimeExecutable": "cmd",
|
||||||
|
"runtimeArgs": ["/c", "cd ui && npx vite"],
|
||||||
|
"port": 5173
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoVerify": true
|
||||||
|
}
|
||||||
22
VISION.md
Normal file
22
VISION.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# VISION
|
||||||
|
|
||||||
|
This document defines the mandatory project vision for AutoForge. All contributions must align with these principles. PRs that deviate from this vision will be rejected. This file itself is immutable via PR — any PR that modifies VISION.md will be rejected outright.
|
||||||
|
|
||||||
|
## Claude Agent SDK Exclusivity
|
||||||
|
|
||||||
|
AutoForge is a wrapper around the **Claude Agent SDK**. This is a foundational architectural decision, not a preference.
|
||||||
|
|
||||||
|
**What this means:**
|
||||||
|
|
||||||
|
- AutoForge only supports providers, models, and integrations that work through the Claude Agent SDK.
|
||||||
|
- We will not integrate with, accommodate, or add support for other AI SDKs, CLIs, or coding agent platforms (e.g., Codex, OpenCode, Aider, Continue, Cursor agents, or similar tools).
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
|
||||||
|
Each platform has its own approach to MCP tools, skills, context management, and feature integration. Attempting to support multiple agent frameworks creates an unsustainable maintenance burden and dilutes the quality of the core experience. By committing to the Claude Agent SDK exclusively, we can build deep, reliable integration rather than shallow compatibility across many targets.
|
||||||
|
|
||||||
|
**In practice:**
|
||||||
|
|
||||||
|
- PRs adding support for non-Claude agent frameworks will be rejected.
|
||||||
|
- PRs introducing abstractions designed to make AutoForge "agent-agnostic" will be rejected.
|
||||||
|
- Alternative API providers (e.g., Vertex AI, AWS Bedrock) are acceptable only when accessed through the Claude Agent SDK's own configuration.
|
||||||
19
agent.py
19
agent.py
@@ -74,7 +74,15 @@ async def run_agent_session(
|
|||||||
await client.query(message)
|
await client.query(message)
|
||||||
|
|
||||||
# Collect response text and show tool use
|
# Collect response text and show tool use
|
||||||
|
# Retry receive_response() on MessageParseError — the SDK raises this for
|
||||||
|
# unknown CLI message types (e.g. "rate_limit_event") which kills the async
|
||||||
|
# generator. The subprocess is still alive so we restart to read remaining
|
||||||
|
# messages from the buffered channel.
|
||||||
response_text = ""
|
response_text = ""
|
||||||
|
max_parse_retries = 50
|
||||||
|
parse_retries = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
async for msg in client.receive_response():
|
async for msg in client.receive_response():
|
||||||
msg_type = type(msg).__name__
|
msg_type = type(msg).__name__
|
||||||
|
|
||||||
@@ -115,6 +123,17 @@ async def run_agent_session(
|
|||||||
# Tool succeeded - just show brief confirmation
|
# Tool succeeded - just show brief confirmation
|
||||||
print(" [Done]", flush=True)
|
print(" [Done]", flush=True)
|
||||||
|
|
||||||
|
break # Normal completion
|
||||||
|
except Exception as inner_exc:
|
||||||
|
if type(inner_exc).__name__ == "MessageParseError":
|
||||||
|
parse_retries += 1
|
||||||
|
if parse_retries > max_parse_retries:
|
||||||
|
print(f"Too many unrecognized CLI messages ({parse_retries}), stopping")
|
||||||
|
break
|
||||||
|
print(f"Ignoring unrecognized message from Claude CLI: {inner_exc}")
|
||||||
|
continue
|
||||||
|
raise # Re-raise to outer except
|
||||||
|
|
||||||
print("\n" + "-" * 70 + "\n")
|
print("\n" + "-" * 70 + "\n")
|
||||||
return "continue", response_text
|
return "continue", response_text
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "autoforge-ai",
|
"name": "autoforge-ai",
|
||||||
"version": "0.1.13",
|
"version": "0.1.15",
|
||||||
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Production runtime dependencies only
|
# Production runtime dependencies only
|
||||||
# For development, use requirements.txt (includes ruff, mypy, pytest)
|
# For development, use requirements.txt (includes ruff, mypy, pytest)
|
||||||
claude-agent-sdk>=0.1.0,<0.2.0
|
claude-agent-sdk>=0.1.39,<0.2.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
sqlalchemy>=2.0.0
|
sqlalchemy>=2.0.0
|
||||||
fastapi>=0.115.0
|
fastapi>=0.115.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
claude-agent-sdk>=0.1.0,<0.2.0
|
claude-agent-sdk>=0.1.39,<0.2.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
sqlalchemy>=2.0.0
|
sqlalchemy>=2.0.0
|
||||||
fastapi>=0.115.0
|
fastapi>=0.115.0
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ from .assistant_database import (
|
|||||||
create_conversation,
|
create_conversation,
|
||||||
get_messages,
|
get_messages,
|
||||||
)
|
)
|
||||||
from .chat_constants import ROOT_DIR
|
from .chat_constants import (
|
||||||
|
ROOT_DIR,
|
||||||
|
check_rate_limit_error,
|
||||||
|
safe_receive_response,
|
||||||
|
)
|
||||||
|
|
||||||
# Load environment variables from .env file if present
|
# Load environment variables from .env file if present
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -394,7 +398,8 @@ class AssistantChatSession:
|
|||||||
full_response = ""
|
full_response = ""
|
||||||
|
|
||||||
# Stream the response
|
# Stream the response
|
||||||
async for msg in self.client.receive_response():
|
try:
|
||||||
|
async for msg in safe_receive_response(self.client, logger):
|
||||||
msg_type = type(msg).__name__
|
msg_type = type(msg).__name__
|
||||||
|
|
||||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||||
@@ -426,6 +431,13 @@ class AssistantChatSession:
|
|||||||
"tool": tool_name,
|
"tool": tool_name,
|
||||||
"input": tool_input,
|
"input": tool_input,
|
||||||
}
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||||
|
if is_rate_limit:
|
||||||
|
logger.warning(f"Rate limited: {exc}")
|
||||||
|
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
|
||||||
# Store the complete response in the database
|
# Store the complete response in the database
|
||||||
if full_response and self.conversation_id:
|
if full_response and self.conversation_id:
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ project root and is re-exported here for convenience so that existing
|
|||||||
imports (``from .chat_constants import API_ENV_VARS``) continue to work.
|
imports (``from .chat_constants import API_ENV_VARS``) continue to work.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AsyncGenerator
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Root directory of the autoforge project (repository root).
|
# Root directory of the autoforge project (repository root).
|
||||||
@@ -32,6 +33,59 @@ if _root_str not in sys.path:
|
|||||||
# imports continue to work unchanged.
|
# imports continue to work unchanged.
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
from env_constants import API_ENV_VARS # noqa: E402, F401
|
from env_constants import API_ENV_VARS # noqa: E402, F401
|
||||||
|
from rate_limit_utils import is_rate_limit_error, parse_retry_after # noqa: E402, F401
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_rate_limit_error(exc: Exception) -> tuple[bool, int | None]:
|
||||||
|
"""Inspect an exception and determine if it represents a rate-limit.
|
||||||
|
|
||||||
|
Returns ``(is_rate_limit, retry_seconds)``. ``retry_seconds`` is the
|
||||||
|
parsed Retry-After value when available, otherwise ``None`` (caller
|
||||||
|
should use exponential backoff).
|
||||||
|
"""
|
||||||
|
# MessageParseError = unknown CLI message type (e.g. "rate_limit_event").
|
||||||
|
# These are informational events, NOT actual rate limit errors.
|
||||||
|
# The word "rate_limit" in the type name would false-positive the regex.
|
||||||
|
if type(exc).__name__ == "MessageParseError":
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
# For all other exceptions: match error text against known rate-limit patterns
|
||||||
|
exc_str = str(exc)
|
||||||
|
if is_rate_limit_error(exc_str):
|
||||||
|
retry = parse_retry_after(exc_str)
|
||||||
|
return True, retry
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
async def safe_receive_response(client: Any, log: logging.Logger) -> AsyncGenerator:
|
||||||
|
"""Wrap ``client.receive_response()`` to skip ``MessageParseError``.
|
||||||
|
|
||||||
|
The Claude Code CLI may emit message types (e.g. ``rate_limit_event``)
|
||||||
|
that the installed Python SDK does not recognise, causing
|
||||||
|
``MessageParseError`` which kills the async generator. The CLI
|
||||||
|
subprocess is still alive and the SDK uses a buffered memory channel,
|
||||||
|
so we restart ``receive_response()`` to continue reading remaining
|
||||||
|
messages without losing data.
|
||||||
|
"""
|
||||||
|
max_retries = 50
|
||||||
|
retries = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async for msg in client.receive_response():
|
||||||
|
yield msg
|
||||||
|
return # Normal completion
|
||||||
|
except Exception as exc:
|
||||||
|
if type(exc).__name__ == "MessageParseError":
|
||||||
|
retries += 1
|
||||||
|
if retries > max_retries:
|
||||||
|
log.error(f"Too many unrecognized CLI messages ({retries}), stopping")
|
||||||
|
return
|
||||||
|
log.warning(f"Ignoring unrecognized message from Claude CLI: {exc}")
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
|
async def make_multimodal_message(content_blocks: list[dict]) -> AsyncGenerator[dict, None]:
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from ..schemas import ImageAttachment
|
from ..schemas import ImageAttachment
|
||||||
from .chat_constants import ROOT_DIR, make_multimodal_message
|
from .chat_constants import (
|
||||||
|
ROOT_DIR,
|
||||||
|
check_rate_limit_error,
|
||||||
|
make_multimodal_message,
|
||||||
|
safe_receive_response,
|
||||||
|
)
|
||||||
|
|
||||||
# Load environment variables from .env file if present
|
# Load environment variables from .env file if present
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -299,7 +304,8 @@ class ExpandChatSession:
|
|||||||
await self.client.query(message)
|
await self.client.query(message)
|
||||||
|
|
||||||
# Stream the response
|
# Stream the response
|
||||||
async for msg in self.client.receive_response():
|
try:
|
||||||
|
async for msg in safe_receive_response(self.client, logger):
|
||||||
msg_type = type(msg).__name__
|
msg_type = type(msg).__name__
|
||||||
|
|
||||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||||
@@ -316,6 +322,13 @@ class ExpandChatSession:
|
|||||||
"content": text,
|
"content": text,
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
|
except Exception as exc:
|
||||||
|
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||||
|
if is_rate_limit:
|
||||||
|
logger.warning(f"Rate limited: {exc}")
|
||||||
|
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
|
||||||
def get_features_created(self) -> int:
|
def get_features_created(self) -> int:
|
||||||
"""Get the total number of features created in this session."""
|
"""Get the total number of features created in this session."""
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from ..schemas import ImageAttachment
|
from ..schemas import ImageAttachment
|
||||||
from .chat_constants import ROOT_DIR, make_multimodal_message
|
from .chat_constants import (
|
||||||
|
ROOT_DIR,
|
||||||
|
check_rate_limit_error,
|
||||||
|
make_multimodal_message,
|
||||||
|
safe_receive_response,
|
||||||
|
)
|
||||||
|
|
||||||
# Load environment variables from .env file if present
|
# Load environment variables from .env file if present
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -304,8 +309,9 @@ class SpecChatSession:
|
|||||||
# Store paths for the completion message
|
# Store paths for the completion message
|
||||||
spec_path = None
|
spec_path = None
|
||||||
|
|
||||||
# Stream the response using receive_response
|
# Stream the response
|
||||||
async for msg in self.client.receive_response():
|
try:
|
||||||
|
async for msg in safe_receive_response(self.client, logger):
|
||||||
msg_type = type(msg).__name__
|
msg_type = type(msg).__name__
|
||||||
|
|
||||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||||
@@ -415,6 +421,13 @@ class SpecChatSession:
|
|||||||
"type": "spec_complete",
|
"type": "spec_complete",
|
||||||
"path": str(spec_path)
|
"path": str(spec_path)
|
||||||
}
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
is_rate_limit, _ = check_rate_limit_error(exc)
|
||||||
|
if is_rate_limit:
|
||||||
|
logger.warning(f"Rate limited: {exc}")
|
||||||
|
yield {"type": "error", "content": "Rate limited. Please try again later."}
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
|
||||||
def is_complete(self) -> bool:
|
def is_complete(self) -> bool:
|
||||||
"""Check if spec creation is complete."""
|
"""Check if spec creation is complete."""
|
||||||
|
|||||||
2
ui/package-lock.json
generated
2
ui/package-lock.json
generated
@@ -56,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
"..": {
|
"..": {
|
||||||
"name": "autoforge-ai",
|
"name": "autoforge-ai",
|
||||||
"version": "0.1.13",
|
"version": "0.1.15",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"autoforge": "bin/autoforge.js"
|
"autoforge": "bin/autoforge.js"
|
||||||
|
|||||||
Reference in New Issue
Block a user