autoforge-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/check-code.md +32 -0
- package/.claude/commands/checkpoint.md +40 -0
- package/.claude/commands/create-spec.md +613 -0
- package/.claude/commands/expand-project.md +234 -0
- package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
- package/.claude/commands/review-pr.md +75 -0
- package/.claude/templates/app_spec.template.txt +331 -0
- package/.claude/templates/coding_prompt.template.md +265 -0
- package/.claude/templates/initializer_prompt.template.md +354 -0
- package/.claude/templates/testing_prompt.template.md +146 -0
- package/.env.example +64 -0
- package/LICENSE.md +676 -0
- package/README.md +423 -0
- package/agent.py +444 -0
- package/api/__init__.py +10 -0
- package/api/database.py +536 -0
- package/api/dependency_resolver.py +449 -0
- package/api/migration.py +156 -0
- package/auth.py +83 -0
- package/autoforge_paths.py +315 -0
- package/autonomous_agent_demo.py +293 -0
- package/bin/autoforge.js +3 -0
- package/client.py +607 -0
- package/env_constants.py +27 -0
- package/examples/OPTIMIZE_CONFIG.md +230 -0
- package/examples/README.md +531 -0
- package/examples/org_config.yaml +172 -0
- package/examples/project_allowed_commands.yaml +139 -0
- package/lib/cli.js +791 -0
- package/mcp_server/__init__.py +1 -0
- package/mcp_server/feature_mcp.py +988 -0
- package/package.json +53 -0
- package/parallel_orchestrator.py +1800 -0
- package/progress.py +247 -0
- package/prompts.py +427 -0
- package/pyproject.toml +17 -0
- package/rate_limit_utils.py +132 -0
- package/registry.py +614 -0
- package/requirements-prod.txt +14 -0
- package/security.py +959 -0
- package/server/__init__.py +17 -0
- package/server/main.py +261 -0
- package/server/routers/__init__.py +32 -0
- package/server/routers/agent.py +177 -0
- package/server/routers/assistant_chat.py +327 -0
- package/server/routers/devserver.py +309 -0
- package/server/routers/expand_project.py +239 -0
- package/server/routers/features.py +746 -0
- package/server/routers/filesystem.py +514 -0
- package/server/routers/projects.py +524 -0
- package/server/routers/schedules.py +356 -0
- package/server/routers/settings.py +127 -0
- package/server/routers/spec_creation.py +357 -0
- package/server/routers/terminal.py +453 -0
- package/server/schemas.py +593 -0
- package/server/services/__init__.py +36 -0
- package/server/services/assistant_chat_session.py +496 -0
- package/server/services/assistant_database.py +304 -0
- package/server/services/chat_constants.py +57 -0
- package/server/services/dev_server_manager.py +557 -0
- package/server/services/expand_chat_session.py +399 -0
- package/server/services/process_manager.py +657 -0
- package/server/services/project_config.py +475 -0
- package/server/services/scheduler_service.py +683 -0
- package/server/services/spec_chat_session.py +502 -0
- package/server/services/terminal_manager.py +756 -0
- package/server/utils/__init__.py +1 -0
- package/server/utils/process_utils.py +134 -0
- package/server/utils/project_helpers.py +32 -0
- package/server/utils/validation.py +54 -0
- package/server/websocket.py +903 -0
- package/start.py +456 -0
- package/ui/dist/assets/index-8W_wmZzz.js +168 -0
- package/ui/dist/assets/index-B47Ubhox.css +1 -0
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
- package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
- package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
- package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
- package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/ollama.png +0 -0
- package/ui/dist/vite.svg +6 -0
- package/ui/package.json +57 -0
package/agent.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Session Logic
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
Core agent interaction functions for running autonomous coding sessions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import io
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from zoneinfo import ZoneInfo
|
|
16
|
+
|
|
17
|
+
from claude_agent_sdk import ClaudeSDKClient
|
|
18
|
+
|
|
19
|
+
# Fix Windows console encoding for Unicode characters (emoji, etc.)
|
|
20
|
+
# Without this, print() crashes when Claude outputs emoji like ✅
|
|
21
|
+
if sys.platform == "win32":
|
|
22
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace", line_buffering=True)
|
|
23
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace", line_buffering=True)
|
|
24
|
+
|
|
25
|
+
from client import create_client
|
|
26
|
+
from progress import (
|
|
27
|
+
count_passing_tests,
|
|
28
|
+
has_features,
|
|
29
|
+
print_progress_summary,
|
|
30
|
+
print_session_header,
|
|
31
|
+
)
|
|
32
|
+
from prompts import (
|
|
33
|
+
copy_spec_to_project,
|
|
34
|
+
get_batch_feature_prompt,
|
|
35
|
+
get_coding_prompt,
|
|
36
|
+
get_initializer_prompt,
|
|
37
|
+
get_single_feature_prompt,
|
|
38
|
+
get_testing_prompt,
|
|
39
|
+
)
|
|
40
|
+
from rate_limit_utils import (
|
|
41
|
+
calculate_error_backoff,
|
|
42
|
+
calculate_rate_limit_backoff,
|
|
43
|
+
clamp_retry_delay,
|
|
44
|
+
is_rate_limit_error,
|
|
45
|
+
parse_retry_after,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Configuration
|
|
49
|
+
AUTO_CONTINUE_DELAY_SECONDS = 3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def run_agent_session(
|
|
53
|
+
client: ClaudeSDKClient,
|
|
54
|
+
message: str,
|
|
55
|
+
project_dir: Path,
|
|
56
|
+
) -> tuple[str, str]:
|
|
57
|
+
"""
|
|
58
|
+
Run a single agent session using Claude Agent SDK.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
client: Claude SDK client
|
|
62
|
+
message: The prompt to send
|
|
63
|
+
project_dir: Project directory path
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
(status, response_text) where status is:
|
|
67
|
+
- "continue" if agent should continue working
|
|
68
|
+
- "error" if an error occurred
|
|
69
|
+
"""
|
|
70
|
+
print("Sending prompt to Claude Agent SDK...\n")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Send the query
|
|
74
|
+
await client.query(message)
|
|
75
|
+
|
|
76
|
+
# Collect response text and show tool use
|
|
77
|
+
response_text = ""
|
|
78
|
+
async for msg in client.receive_response():
|
|
79
|
+
msg_type = type(msg).__name__
|
|
80
|
+
|
|
81
|
+
# Handle AssistantMessage (text and tool use)
|
|
82
|
+
if msg_type == "AssistantMessage" and hasattr(msg, "content"):
|
|
83
|
+
for block in msg.content:
|
|
84
|
+
block_type = type(block).__name__
|
|
85
|
+
|
|
86
|
+
if block_type == "TextBlock" and hasattr(block, "text"):
|
|
87
|
+
response_text += block.text
|
|
88
|
+
print(block.text, end="", flush=True)
|
|
89
|
+
elif block_type == "ToolUseBlock" and hasattr(block, "name"):
|
|
90
|
+
print(f"\n[Tool: {block.name}]", flush=True)
|
|
91
|
+
if hasattr(block, "input"):
|
|
92
|
+
input_str = str(block.input)
|
|
93
|
+
if len(input_str) > 200:
|
|
94
|
+
print(f" Input: {input_str[:200]}...", flush=True)
|
|
95
|
+
else:
|
|
96
|
+
print(f" Input: {input_str}", flush=True)
|
|
97
|
+
|
|
98
|
+
# Handle UserMessage (tool results)
|
|
99
|
+
elif msg_type == "UserMessage" and hasattr(msg, "content"):
|
|
100
|
+
for block in msg.content:
|
|
101
|
+
block_type = type(block).__name__
|
|
102
|
+
|
|
103
|
+
if block_type == "ToolResultBlock":
|
|
104
|
+
result_content = getattr(block, "content", "")
|
|
105
|
+
is_error = getattr(block, "is_error", False)
|
|
106
|
+
|
|
107
|
+
# Check if command was blocked by security hook
|
|
108
|
+
if "blocked" in str(result_content).lower():
|
|
109
|
+
print(f" [BLOCKED] {result_content}", flush=True)
|
|
110
|
+
elif is_error:
|
|
111
|
+
# Show errors (truncated)
|
|
112
|
+
error_str = str(result_content)[:500]
|
|
113
|
+
print(f" [Error] {error_str}", flush=True)
|
|
114
|
+
else:
|
|
115
|
+
# Tool succeeded - just show brief confirmation
|
|
116
|
+
print(" [Done]", flush=True)
|
|
117
|
+
|
|
118
|
+
print("\n" + "-" * 70 + "\n")
|
|
119
|
+
return "continue", response_text
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
error_str = str(e)
|
|
123
|
+
print(f"Error during agent session: {error_str}")
|
|
124
|
+
|
|
125
|
+
# Detect rate limit errors from exception message
|
|
126
|
+
if is_rate_limit_error(error_str):
|
|
127
|
+
# Try to extract retry-after time from error
|
|
128
|
+
retry_seconds = parse_retry_after(error_str)
|
|
129
|
+
if retry_seconds is not None:
|
|
130
|
+
return "rate_limit", str(retry_seconds)
|
|
131
|
+
else:
|
|
132
|
+
return "rate_limit", "unknown"
|
|
133
|
+
|
|
134
|
+
return "error", error_str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def run_autonomous_agent(
|
|
138
|
+
project_dir: Path,
|
|
139
|
+
model: str,
|
|
140
|
+
max_iterations: Optional[int] = None,
|
|
141
|
+
yolo_mode: bool = False,
|
|
142
|
+
feature_id: Optional[int] = None,
|
|
143
|
+
feature_ids: Optional[list[int]] = None,
|
|
144
|
+
agent_type: Optional[str] = None,
|
|
145
|
+
testing_feature_id: Optional[int] = None,
|
|
146
|
+
testing_feature_ids: Optional[list[int]] = None,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Run the autonomous agent loop.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
project_dir: Directory for the project
|
|
153
|
+
model: Claude model to use
|
|
154
|
+
max_iterations: Maximum number of iterations (None for unlimited)
|
|
155
|
+
yolo_mode: If True, skip browser testing in coding agent prompts
|
|
156
|
+
feature_id: If set, work only on this specific feature (used by orchestrator for coding agents)
|
|
157
|
+
feature_ids: If set, work on these features in batch (used by orchestrator for batch mode)
|
|
158
|
+
agent_type: Type of agent: "initializer", "coding", "testing", or None (auto-detect)
|
|
159
|
+
testing_feature_id: For testing agents, the pre-claimed feature ID to test (legacy single mode)
|
|
160
|
+
testing_feature_ids: For testing agents, list of feature IDs to batch test
|
|
161
|
+
"""
|
|
162
|
+
print("\n" + "=" * 70)
|
|
163
|
+
print(" AUTONOMOUS CODING AGENT")
|
|
164
|
+
print("=" * 70)
|
|
165
|
+
print(f"\nProject directory: {project_dir}")
|
|
166
|
+
print(f"Model: {model}")
|
|
167
|
+
if agent_type:
|
|
168
|
+
print(f"Agent type: {agent_type}")
|
|
169
|
+
if yolo_mode:
|
|
170
|
+
print("Mode: YOLO (testing agents disabled)")
|
|
171
|
+
if feature_ids and len(feature_ids) > 1:
|
|
172
|
+
print(f"Feature batch: {', '.join(f'#{fid}' for fid in feature_ids)}")
|
|
173
|
+
elif feature_id:
|
|
174
|
+
print(f"Feature assignment: #{feature_id}")
|
|
175
|
+
if max_iterations:
|
|
176
|
+
print(f"Max iterations: {max_iterations}")
|
|
177
|
+
else:
|
|
178
|
+
print("Max iterations: Unlimited (will run until completion)")
|
|
179
|
+
print()
|
|
180
|
+
|
|
181
|
+
# Create project directory
|
|
182
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
|
|
184
|
+
# Determine agent type if not explicitly set
|
|
185
|
+
if agent_type is None:
|
|
186
|
+
# Auto-detect based on whether we have features
|
|
187
|
+
# (This path is for legacy compatibility - orchestrator should always set agent_type)
|
|
188
|
+
is_first_run = not has_features(project_dir)
|
|
189
|
+
if is_first_run:
|
|
190
|
+
agent_type = "initializer"
|
|
191
|
+
else:
|
|
192
|
+
agent_type = "coding"
|
|
193
|
+
|
|
194
|
+
is_initializer = agent_type == "initializer"
|
|
195
|
+
|
|
196
|
+
if is_initializer:
|
|
197
|
+
print("Running as INITIALIZER agent")
|
|
198
|
+
print()
|
|
199
|
+
print("=" * 70)
|
|
200
|
+
print(" NOTE: Initialization takes 10-20+ minutes!")
|
|
201
|
+
print(" The agent is generating detailed test cases.")
|
|
202
|
+
print(" This may appear to hang - it's working. Watch for [Tool: ...] output.")
|
|
203
|
+
print("=" * 70)
|
|
204
|
+
print()
|
|
205
|
+
# Copy the app spec into the project directory for the agent to read
|
|
206
|
+
copy_spec_to_project(project_dir)
|
|
207
|
+
elif agent_type == "testing":
|
|
208
|
+
print("Running as TESTING agent (regression testing)")
|
|
209
|
+
print_progress_summary(project_dir)
|
|
210
|
+
else:
|
|
211
|
+
print("Running as CODING agent")
|
|
212
|
+
print_progress_summary(project_dir)
|
|
213
|
+
|
|
214
|
+
# Main loop
|
|
215
|
+
iteration = 0
|
|
216
|
+
rate_limit_retries = 0 # Track consecutive rate limit errors for exponential backoff
|
|
217
|
+
error_retries = 0 # Track consecutive non-rate-limit errors
|
|
218
|
+
|
|
219
|
+
while True:
|
|
220
|
+
iteration += 1
|
|
221
|
+
|
|
222
|
+
# Check if all features are already complete (before starting a new session)
|
|
223
|
+
# Skip this check if running as initializer (needs to create features first)
|
|
224
|
+
if not is_initializer and iteration == 1:
|
|
225
|
+
passing, in_progress, total = count_passing_tests(project_dir)
|
|
226
|
+
if total > 0 and passing == total:
|
|
227
|
+
print("\n" + "=" * 70)
|
|
228
|
+
print(" ALL FEATURES ALREADY COMPLETE!")
|
|
229
|
+
print("=" * 70)
|
|
230
|
+
print(f"\nAll {total} features are passing. Nothing left to do.")
|
|
231
|
+
break
|
|
232
|
+
|
|
233
|
+
# Check max iterations
|
|
234
|
+
if max_iterations and iteration > max_iterations:
|
|
235
|
+
print(f"\nReached max iterations ({max_iterations})")
|
|
236
|
+
print("To continue, run the script again without --max-iterations")
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
# Print session header
|
|
240
|
+
print_session_header(iteration, is_initializer)
|
|
241
|
+
|
|
242
|
+
# Create client (fresh context)
|
|
243
|
+
# Pass agent_id for browser isolation in multi-agent scenarios
|
|
244
|
+
import os
|
|
245
|
+
if agent_type == "testing":
|
|
246
|
+
agent_id = f"testing-{os.getpid()}" # Unique ID for testing agents
|
|
247
|
+
elif feature_ids and len(feature_ids) > 1:
|
|
248
|
+
agent_id = f"batch-{feature_ids[0]}"
|
|
249
|
+
elif feature_id:
|
|
250
|
+
agent_id = f"feature-{feature_id}"
|
|
251
|
+
else:
|
|
252
|
+
agent_id = None
|
|
253
|
+
client = create_client(project_dir, model, yolo_mode=yolo_mode, agent_id=agent_id, agent_type=agent_type)
|
|
254
|
+
|
|
255
|
+
# Choose prompt based on agent type
|
|
256
|
+
if agent_type == "initializer":
|
|
257
|
+
prompt = get_initializer_prompt(project_dir)
|
|
258
|
+
elif agent_type == "testing":
|
|
259
|
+
prompt = get_testing_prompt(project_dir, testing_feature_id, testing_feature_ids)
|
|
260
|
+
elif feature_ids and len(feature_ids) > 1:
|
|
261
|
+
# Batch mode (used by orchestrator for multi-feature coding agents)
|
|
262
|
+
prompt = get_batch_feature_prompt(feature_ids, project_dir, yolo_mode)
|
|
263
|
+
elif feature_id or (feature_ids is not None and len(feature_ids) == 1):
|
|
264
|
+
# Single-feature mode (used by orchestrator for coding agents)
|
|
265
|
+
fid = feature_id if feature_id is not None else feature_ids[0] # type: ignore[index]
|
|
266
|
+
prompt = get_single_feature_prompt(fid, project_dir, yolo_mode)
|
|
267
|
+
else:
|
|
268
|
+
# General coding prompt (legacy path)
|
|
269
|
+
prompt = get_coding_prompt(project_dir, yolo_mode=yolo_mode)
|
|
270
|
+
|
|
271
|
+
# Run session with async context manager
|
|
272
|
+
# Wrap in try/except to handle MCP server startup failures gracefully
|
|
273
|
+
try:
|
|
274
|
+
async with client:
|
|
275
|
+
status, response = await run_agent_session(client, prompt, project_dir)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Client/MCP server error: {e}")
|
|
278
|
+
# Don't crash - return error status so the loop can retry
|
|
279
|
+
status, response = "error", str(e)
|
|
280
|
+
|
|
281
|
+
# Check for project completion - EXIT when all features pass
|
|
282
|
+
if "all features are passing" in response.lower() or "no more work to do" in response.lower():
|
|
283
|
+
print("\n" + "=" * 70)
|
|
284
|
+
print(" 🎉 PROJECT COMPLETE - ALL FEATURES PASSING!")
|
|
285
|
+
print("=" * 70)
|
|
286
|
+
print_progress_summary(project_dir)
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
# Handle status
|
|
290
|
+
if status == "continue":
|
|
291
|
+
# Reset error retries on success; rate-limit retries reset only if no signal
|
|
292
|
+
error_retries = 0
|
|
293
|
+
reset_rate_limit_retries = True
|
|
294
|
+
|
|
295
|
+
delay_seconds = AUTO_CONTINUE_DELAY_SECONDS
|
|
296
|
+
target_time_str = None
|
|
297
|
+
|
|
298
|
+
# Check for rate limit indicators in response text
|
|
299
|
+
if is_rate_limit_error(response):
|
|
300
|
+
print("Claude Agent SDK indicated rate limit reached.")
|
|
301
|
+
reset_rate_limit_retries = False
|
|
302
|
+
|
|
303
|
+
# Try to extract retry-after from response text first
|
|
304
|
+
retry_seconds = parse_retry_after(response)
|
|
305
|
+
if retry_seconds is not None:
|
|
306
|
+
delay_seconds = clamp_retry_delay(retry_seconds)
|
|
307
|
+
else:
|
|
308
|
+
# Use exponential backoff when retry-after unknown
|
|
309
|
+
delay_seconds = calculate_rate_limit_backoff(rate_limit_retries)
|
|
310
|
+
rate_limit_retries += 1
|
|
311
|
+
|
|
312
|
+
# Try to parse reset time from response (more specific format)
|
|
313
|
+
match = re.search(
|
|
314
|
+
r"(?i)\bresets(?:\s+at)?\s+(\d+)(?::(\d+))?\s*(am|pm)\s*\(([^)]+)\)",
|
|
315
|
+
response,
|
|
316
|
+
)
|
|
317
|
+
if match:
|
|
318
|
+
hour = int(match.group(1))
|
|
319
|
+
minute = int(match.group(2)) if match.group(2) else 0
|
|
320
|
+
period = match.group(3).lower()
|
|
321
|
+
tz_name = match.group(4).strip()
|
|
322
|
+
|
|
323
|
+
# Convert to 24-hour format
|
|
324
|
+
if period == "pm" and hour != 12:
|
|
325
|
+
hour += 12
|
|
326
|
+
elif period == "am" and hour == 12:
|
|
327
|
+
hour = 0
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
tz = ZoneInfo(tz_name)
|
|
331
|
+
now = datetime.now(tz)
|
|
332
|
+
target = now.replace(
|
|
333
|
+
hour=hour, minute=minute, second=0, microsecond=0
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# If target time has already passed today, wait until tomorrow
|
|
337
|
+
if target <= now:
|
|
338
|
+
target += timedelta(days=1)
|
|
339
|
+
|
|
340
|
+
delta = target - now
|
|
341
|
+
delay_seconds = min(max(int(delta.total_seconds()), 1), 24 * 60 * 60)
|
|
342
|
+
target_time_str = target.strftime("%B %d, %Y at %I:%M %p %Z")
|
|
343
|
+
|
|
344
|
+
except Exception as e:
|
|
345
|
+
print(f"Error parsing reset time: {e}, using default delay")
|
|
346
|
+
|
|
347
|
+
if target_time_str:
|
|
348
|
+
print(
|
|
349
|
+
f"\nClaude Code Limit Reached. Agent will auto-continue in {delay_seconds:.0f}s ({target_time_str})...",
|
|
350
|
+
flush=True,
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
print(
|
|
354
|
+
f"\nAgent will auto-continue in {delay_seconds:.0f}s...", flush=True
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
sys.stdout.flush() # this should allow the pause to be displayed before sleeping
|
|
358
|
+
print_progress_summary(project_dir)
|
|
359
|
+
|
|
360
|
+
# Check if all features are complete - exit gracefully if done
|
|
361
|
+
passing, in_progress, total = count_passing_tests(project_dir)
|
|
362
|
+
if total > 0 and passing == total:
|
|
363
|
+
print("\n" + "=" * 70)
|
|
364
|
+
print(" ALL FEATURES COMPLETE!")
|
|
365
|
+
print("=" * 70)
|
|
366
|
+
print(f"\nCongratulations! All {total} features are passing.")
|
|
367
|
+
print("The autonomous agent has finished its work.")
|
|
368
|
+
break
|
|
369
|
+
|
|
370
|
+
# Single-feature mode, batch mode, or testing agent: exit after one session
|
|
371
|
+
if feature_ids and len(feature_ids) > 1:
|
|
372
|
+
print(f"\nBatch mode: Features {', '.join(f'#{fid}' for fid in feature_ids)} session complete.")
|
|
373
|
+
break
|
|
374
|
+
elif feature_id is not None or (feature_ids is not None and len(feature_ids) == 1):
|
|
375
|
+
fid = feature_id if feature_id is not None else feature_ids[0] # type: ignore[index]
|
|
376
|
+
if agent_type == "testing":
|
|
377
|
+
print("\nTesting agent complete. Terminating session.")
|
|
378
|
+
else:
|
|
379
|
+
print(f"\nSingle-feature mode: Feature #{fid} session complete.")
|
|
380
|
+
break
|
|
381
|
+
elif agent_type == "testing":
|
|
382
|
+
print("\nTesting agent complete. Terminating session.")
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
# Reset rate limit retries only if no rate limit signal was detected
|
|
386
|
+
if reset_rate_limit_retries:
|
|
387
|
+
rate_limit_retries = 0
|
|
388
|
+
|
|
389
|
+
await asyncio.sleep(delay_seconds)
|
|
390
|
+
|
|
391
|
+
elif status == "rate_limit":
|
|
392
|
+
# Smart rate limit handling with exponential backoff
|
|
393
|
+
# Reset error counter so mixed events don't inflate delays
|
|
394
|
+
error_retries = 0
|
|
395
|
+
if response != "unknown":
|
|
396
|
+
try:
|
|
397
|
+
delay_seconds = clamp_retry_delay(int(response))
|
|
398
|
+
except (ValueError, TypeError):
|
|
399
|
+
# Malformed value - fall through to exponential backoff
|
|
400
|
+
response = "unknown"
|
|
401
|
+
if response == "unknown":
|
|
402
|
+
# Use exponential backoff when retry-after unknown or malformed
|
|
403
|
+
delay_seconds = calculate_rate_limit_backoff(rate_limit_retries)
|
|
404
|
+
rate_limit_retries += 1
|
|
405
|
+
print(f"\nRate limit hit. Backoff wait: {delay_seconds} seconds (attempt #{rate_limit_retries})...")
|
|
406
|
+
else:
|
|
407
|
+
print(f"\nRate limit hit. Waiting {delay_seconds} seconds before retry...")
|
|
408
|
+
|
|
409
|
+
await asyncio.sleep(delay_seconds)
|
|
410
|
+
|
|
411
|
+
elif status == "error":
|
|
412
|
+
# Non-rate-limit errors: linear backoff capped at 5 minutes
|
|
413
|
+
# Reset rate limit counter so mixed events don't inflate delays
|
|
414
|
+
rate_limit_retries = 0
|
|
415
|
+
error_retries += 1
|
|
416
|
+
delay_seconds = calculate_error_backoff(error_retries)
|
|
417
|
+
print("\nSession encountered an error")
|
|
418
|
+
print(f"Will retry in {delay_seconds}s (attempt #{error_retries})...")
|
|
419
|
+
await asyncio.sleep(delay_seconds)
|
|
420
|
+
|
|
421
|
+
# Small delay between sessions
|
|
422
|
+
if max_iterations is None or iteration < max_iterations:
|
|
423
|
+
print("\nPreparing next session...\n")
|
|
424
|
+
await asyncio.sleep(1)
|
|
425
|
+
|
|
426
|
+
# Final summary
|
|
427
|
+
print("\n" + "=" * 70)
|
|
428
|
+
print(" SESSION COMPLETE")
|
|
429
|
+
print("=" * 70)
|
|
430
|
+
print(f"\nProject directory: {project_dir}")
|
|
431
|
+
print_progress_summary(project_dir)
|
|
432
|
+
|
|
433
|
+
# Print instructions for running the generated application
|
|
434
|
+
print("\n" + "-" * 70)
|
|
435
|
+
print(" TO RUN THE GENERATED APPLICATION:")
|
|
436
|
+
print("-" * 70)
|
|
437
|
+
print(f"\n cd {project_dir.resolve()}")
|
|
438
|
+
print(" ./init.sh # Run the setup script")
|
|
439
|
+
print(" # Or manually:")
|
|
440
|
+
print(" npm install && npm run dev")
|
|
441
|
+
print("\n Then open http://localhost:3000 (or check init.sh for the URL)")
|
|
442
|
+
print("-" * 70)
|
|
443
|
+
|
|
444
|
+
print("\nDone!")
|
package/api/__init__.py
ADDED