mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-21 04:43:09 +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
|
||||
|
||||
6. **Vision Alignment Check**
|
||||
- Read the project's README.md and CLAUDE.md to understand the application's core purpose
|
||||
- Assess whether this PR aligns with the application's intended functionality
|
||||
- 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
|
||||
- This is not a blocker, but should be flagged for the reviewer's consideration
|
||||
- **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.
|
||||
- Read the project's `VISION.md`, `README.md`, and `CLAUDE.md` to understand the application's core purpose and mandatory architectural constraints
|
||||
- Assess whether this PR aligns with the vision defined in `VISION.md`
|
||||
- **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**
|
||||
- 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.
|
||||
87
agent.py
87
agent.py
@@ -74,46 +74,65 @@ async def run_agent_session(
|
||||
await client.query(message)
|
||||
|
||||
# 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 = ""
|
||||
async for msg in client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
max_parse_retries = 50
|
||||
parse_retries = 0
|
||||
while True:
|
||||
try:
|
||||
async for msg in client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
# Handle AssistantMessage (text and tool use)
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
# Handle AssistantMessage (text and tool use)
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
response_text += block.text
|
||||
print(block.text, end="", flush=True)
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
print(f"\n[Tool: {block.name}]", flush=True)
|
||||
if hasattr(block, "input"):
|
||||
input_str = str(block.input)
|
||||
if len(input_str) > 200:
|
||||
print(f" Input: {input_str[:200]}...", flush=True)
|
||||
else:
|
||||
print(f" Input: {input_str}", flush=True)
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
response_text += block.text
|
||||
print(block.text, end="", flush=True)
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
print(f"\n[Tool: {block.name}]", flush=True)
|
||||
if hasattr(block, "input"):
|
||||
input_str = str(block.input)
|
||||
if len(input_str) > 200:
|
||||
print(f" Input: {input_str[:200]}...", flush=True)
|
||||
else:
|
||||
print(f" Input: {input_str}", flush=True)
|
||||
|
||||
# Handle UserMessage (tool results)
|
||||
elif msg_type == "UserMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
# Handle UserMessage (tool results)
|
||||
elif msg_type == "UserMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
|
||||
if block_type == "ToolResultBlock":
|
||||
result_content = getattr(block, "content", "")
|
||||
is_error = getattr(block, "is_error", False)
|
||||
if block_type == "ToolResultBlock":
|
||||
result_content = getattr(block, "content", "")
|
||||
is_error = getattr(block, "is_error", False)
|
||||
|
||||
# Check if command was blocked by security hook
|
||||
if "blocked" in str(result_content).lower():
|
||||
print(f" [BLOCKED] {result_content}", flush=True)
|
||||
elif is_error:
|
||||
# Show errors (truncated)
|
||||
error_str = str(result_content)[:500]
|
||||
print(f" [Error] {error_str}", flush=True)
|
||||
else:
|
||||
# Tool succeeded - just show brief confirmation
|
||||
print(" [Done]", flush=True)
|
||||
# Check if command was blocked by security hook
|
||||
if "blocked" in str(result_content).lower():
|
||||
print(f" [BLOCKED] {result_content}", flush=True)
|
||||
elif is_error:
|
||||
# Show errors (truncated)
|
||||
error_str = str(result_content)[:500]
|
||||
print(f" [Error] {error_str}", flush=True)
|
||||
else:
|
||||
# Tool succeeded - just show brief confirmation
|
||||
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")
|
||||
return "continue", response_text
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.15",
|
||||
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Production runtime dependencies only
|
||||
# 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
|
||||
sqlalchemy>=2.0.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
|
||||
sqlalchemy>=2.0.0
|
||||
fastapi>=0.115.0
|
||||
|
||||
@@ -25,7 +25,11 @@ from .assistant_database import (
|
||||
create_conversation,
|
||||
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_dotenv()
|
||||
@@ -394,38 +398,46 @@ class AssistantChatSession:
|
||||
full_response = ""
|
||||
|
||||
# Stream the response
|
||||
async for msg in self.client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
text = block.text
|
||||
if text:
|
||||
full_response += text
|
||||
yield {"type": "text", "content": text}
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
text = block.text
|
||||
if text:
|
||||
full_response += text
|
||||
yield {"type": "text", "content": text}
|
||||
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
tool_name = block.name
|
||||
tool_input = getattr(block, "input", {})
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
tool_name = block.name
|
||||
tool_input = getattr(block, "input", {})
|
||||
|
||||
# Intercept ask_user tool calls -> yield as question message
|
||||
if tool_name == "mcp__features__ask_user":
|
||||
questions = tool_input.get("questions", [])
|
||||
if questions:
|
||||
yield {
|
||||
"type": "question",
|
||||
"questions": questions,
|
||||
}
|
||||
continue
|
||||
# Intercept ask_user tool calls -> yield as question message
|
||||
if tool_name == "mcp__features__ask_user":
|
||||
questions = tool_input.get("questions", [])
|
||||
if questions:
|
||||
yield {
|
||||
"type": "question",
|
||||
"questions": questions,
|
||||
}
|
||||
continue
|
||||
|
||||
yield {
|
||||
"type": "tool_call",
|
||||
"tool": tool_name,
|
||||
"input": tool_input,
|
||||
}
|
||||
yield {
|
||||
"type": "tool_call",
|
||||
"tool": tool_name,
|
||||
"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
|
||||
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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Root directory of the autoforge project (repository root).
|
||||
@@ -32,6 +33,59 @@ if _root_str not in sys.path:
|
||||
# imports continue to work unchanged.
|
||||
# -------------------------------------------------------------------
|
||||
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]:
|
||||
|
||||
@@ -22,7 +22,12 @@ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
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_dotenv()
|
||||
@@ -299,23 +304,31 @@ class ExpandChatSession:
|
||||
await self.client.query(message)
|
||||
|
||||
# Stream the response
|
||||
async for msg in self.client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
text = block.text
|
||||
if text:
|
||||
yield {"type": "text", "content": text}
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
text = block.text
|
||||
if text:
|
||||
yield {"type": "text", "content": text}
|
||||
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
"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:
|
||||
"""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 ..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_dotenv()
|
||||
@@ -304,117 +309,125 @@ class SpecChatSession:
|
||||
# Store paths for the completion message
|
||||
spec_path = None
|
||||
|
||||
# Stream the response using receive_response
|
||||
async for msg in self.client.receive_response():
|
||||
msg_type = type(msg).__name__
|
||||
# Stream the response
|
||||
try:
|
||||
async for msg in safe_receive_response(self.client, logger):
|
||||
msg_type = type(msg).__name__
|
||||
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
# Process content blocks in the assistant message
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
||||
# Process content blocks in the assistant message
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
# Accumulate text and yield it
|
||||
text = block.text
|
||||
if text:
|
||||
current_text += text
|
||||
yield {"type": "text", "content": text}
|
||||
if block_type == "TextBlock" and hasattr(block, "text"):
|
||||
# Accumulate text and yield it
|
||||
text = block.text
|
||||
if text:
|
||||
current_text += text
|
||||
yield {"type": "text", "content": text}
|
||||
|
||||
# Store in message history
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
# Store in message history
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": text,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
tool_name = block.name
|
||||
tool_input = getattr(block, "input", {})
|
||||
tool_id = getattr(block, "id", "")
|
||||
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
||||
tool_name = block.name
|
||||
tool_input = getattr(block, "input", {})
|
||||
tool_id = getattr(block, "id", "")
|
||||
|
||||
if tool_name in ("Write", "Edit"):
|
||||
# File being written or edited - track for verification
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if tool_name in ("Write", "Edit"):
|
||||
# File being written or edited - track for verification
|
||||
file_path = tool_input.get("file_path", "")
|
||||
|
||||
# Track app_spec.txt
|
||||
if "app_spec.txt" in str(file_path):
|
||||
pending_writes["app_spec"] = {
|
||||
"tool_id": tool_id,
|
||||
"path": file_path
|
||||
}
|
||||
logger.info(f"{tool_name} tool called for app_spec.txt: {file_path}")
|
||||
|
||||
# Track initializer_prompt.md
|
||||
elif "initializer_prompt.md" in str(file_path):
|
||||
pending_writes["initializer"] = {
|
||||
"tool_id": tool_id,
|
||||
"path": file_path
|
||||
}
|
||||
logger.info(f"{tool_name} tool called for initializer_prompt.md: {file_path}")
|
||||
|
||||
elif msg_type == "UserMessage" and hasattr(msg, "content"):
|
||||
# Tool results - check for write confirmations and errors
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
if block_type == "ToolResultBlock":
|
||||
is_error = getattr(block, "is_error", False)
|
||||
tool_use_id = getattr(block, "tool_use_id", "")
|
||||
|
||||
if is_error:
|
||||
content = getattr(block, "content", "Unknown error")
|
||||
logger.warning(f"Tool error: {content}")
|
||||
# Clear any pending writes that failed
|
||||
for key in pending_writes:
|
||||
pending_write = pending_writes[key]
|
||||
if pending_write is not None and tool_use_id == pending_write.get("tool_id"):
|
||||
logger.error(f"{key} write failed: {content}")
|
||||
pending_writes[key] = None
|
||||
else:
|
||||
# Tool succeeded - check which file was written
|
||||
|
||||
# Check app_spec.txt
|
||||
if pending_writes["app_spec"] and tool_use_id == pending_writes["app_spec"].get("tool_id"):
|
||||
file_path = pending_writes["app_spec"]["path"]
|
||||
full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
|
||||
if full_path.exists():
|
||||
logger.info(f"app_spec.txt verified at: {full_path}")
|
||||
files_written["app_spec"] = True
|
||||
spec_path = file_path
|
||||
|
||||
# Notify about file write (but NOT completion yet)
|
||||
yield {
|
||||
"type": "file_written",
|
||||
"path": str(file_path)
|
||||
# Track app_spec.txt
|
||||
if "app_spec.txt" in str(file_path):
|
||||
pending_writes["app_spec"] = {
|
||||
"tool_id": tool_id,
|
||||
"path": file_path
|
||||
}
|
||||
else:
|
||||
logger.error(f"app_spec.txt not found after write: {full_path}")
|
||||
pending_writes["app_spec"] = None
|
||||
logger.info(f"{tool_name} tool called for app_spec.txt: {file_path}")
|
||||
|
||||
# Check initializer_prompt.md
|
||||
if pending_writes["initializer"] and tool_use_id == pending_writes["initializer"].get("tool_id"):
|
||||
file_path = pending_writes["initializer"]["path"]
|
||||
full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
|
||||
if full_path.exists():
|
||||
logger.info(f"initializer_prompt.md verified at: {full_path}")
|
||||
files_written["initializer"] = True
|
||||
|
||||
# Notify about file write
|
||||
yield {
|
||||
"type": "file_written",
|
||||
"path": str(file_path)
|
||||
# Track initializer_prompt.md
|
||||
elif "initializer_prompt.md" in str(file_path):
|
||||
pending_writes["initializer"] = {
|
||||
"tool_id": tool_id,
|
||||
"path": file_path
|
||||
}
|
||||
else:
|
||||
logger.error(f"initializer_prompt.md not found after write: {full_path}")
|
||||
pending_writes["initializer"] = None
|
||||
logger.info(f"{tool_name} tool called for initializer_prompt.md: {file_path}")
|
||||
|
||||
# Check if BOTH files are now written - only then signal completion
|
||||
if files_written["app_spec"] and files_written["initializer"]:
|
||||
logger.info("Both app_spec.txt and initializer_prompt.md verified - signaling completion")
|
||||
self.complete = True
|
||||
yield {
|
||||
"type": "spec_complete",
|
||||
"path": str(spec_path)
|
||||
}
|
||||
elif msg_type == "UserMessage" and hasattr(msg, "content"):
|
||||
# Tool results - check for write confirmations and errors
|
||||
for block in msg.content:
|
||||
block_type = type(block).__name__
|
||||
if block_type == "ToolResultBlock":
|
||||
is_error = getattr(block, "is_error", False)
|
||||
tool_use_id = getattr(block, "tool_use_id", "")
|
||||
|
||||
if is_error:
|
||||
content = getattr(block, "content", "Unknown error")
|
||||
logger.warning(f"Tool error: {content}")
|
||||
# Clear any pending writes that failed
|
||||
for key in pending_writes:
|
||||
pending_write = pending_writes[key]
|
||||
if pending_write is not None and tool_use_id == pending_write.get("tool_id"):
|
||||
logger.error(f"{key} write failed: {content}")
|
||||
pending_writes[key] = None
|
||||
else:
|
||||
# Tool succeeded - check which file was written
|
||||
|
||||
# Check app_spec.txt
|
||||
if pending_writes["app_spec"] and tool_use_id == pending_writes["app_spec"].get("tool_id"):
|
||||
file_path = pending_writes["app_spec"]["path"]
|
||||
full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
|
||||
if full_path.exists():
|
||||
logger.info(f"app_spec.txt verified at: {full_path}")
|
||||
files_written["app_spec"] = True
|
||||
spec_path = file_path
|
||||
|
||||
# Notify about file write (but NOT completion yet)
|
||||
yield {
|
||||
"type": "file_written",
|
||||
"path": str(file_path)
|
||||
}
|
||||
else:
|
||||
logger.error(f"app_spec.txt not found after write: {full_path}")
|
||||
pending_writes["app_spec"] = None
|
||||
|
||||
# Check initializer_prompt.md
|
||||
if pending_writes["initializer"] and tool_use_id == pending_writes["initializer"].get("tool_id"):
|
||||
file_path = pending_writes["initializer"]["path"]
|
||||
full_path = Path(file_path) if Path(file_path).is_absolute() else self.project_dir / file_path
|
||||
if full_path.exists():
|
||||
logger.info(f"initializer_prompt.md verified at: {full_path}")
|
||||
files_written["initializer"] = True
|
||||
|
||||
# Notify about file write
|
||||
yield {
|
||||
"type": "file_written",
|
||||
"path": str(file_path)
|
||||
}
|
||||
else:
|
||||
logger.error(f"initializer_prompt.md not found after write: {full_path}")
|
||||
pending_writes["initializer"] = None
|
||||
|
||||
# Check if BOTH files are now written - only then signal completion
|
||||
if files_written["app_spec"] and files_written["initializer"]:
|
||||
logger.info("Both app_spec.txt and initializer_prompt.md verified - signaling completion")
|
||||
self.complete = True
|
||||
yield {
|
||||
"type": "spec_complete",
|
||||
"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:
|
||||
"""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",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.15",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "bin/autoforge.js"
|
||||
|
||||
Reference in New Issue
Block a user