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.
Files changed (84) hide show
  1. package/.claude/commands/check-code.md +32 -0
  2. package/.claude/commands/checkpoint.md +40 -0
  3. package/.claude/commands/create-spec.md +613 -0
  4. package/.claude/commands/expand-project.md +234 -0
  5. package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
  6. package/.claude/commands/review-pr.md +75 -0
  7. package/.claude/templates/app_spec.template.txt +331 -0
  8. package/.claude/templates/coding_prompt.template.md +265 -0
  9. package/.claude/templates/initializer_prompt.template.md +354 -0
  10. package/.claude/templates/testing_prompt.template.md +146 -0
  11. package/.env.example +64 -0
  12. package/LICENSE.md +676 -0
  13. package/README.md +423 -0
  14. package/agent.py +444 -0
  15. package/api/__init__.py +10 -0
  16. package/api/database.py +536 -0
  17. package/api/dependency_resolver.py +449 -0
  18. package/api/migration.py +156 -0
  19. package/auth.py +83 -0
  20. package/autoforge_paths.py +315 -0
  21. package/autonomous_agent_demo.py +293 -0
  22. package/bin/autoforge.js +3 -0
  23. package/client.py +607 -0
  24. package/env_constants.py +27 -0
  25. package/examples/OPTIMIZE_CONFIG.md +230 -0
  26. package/examples/README.md +531 -0
  27. package/examples/org_config.yaml +172 -0
  28. package/examples/project_allowed_commands.yaml +139 -0
  29. package/lib/cli.js +791 -0
  30. package/mcp_server/__init__.py +1 -0
  31. package/mcp_server/feature_mcp.py +988 -0
  32. package/package.json +53 -0
  33. package/parallel_orchestrator.py +1800 -0
  34. package/progress.py +247 -0
  35. package/prompts.py +427 -0
  36. package/pyproject.toml +17 -0
  37. package/rate_limit_utils.py +132 -0
  38. package/registry.py +614 -0
  39. package/requirements-prod.txt +14 -0
  40. package/security.py +959 -0
  41. package/server/__init__.py +17 -0
  42. package/server/main.py +261 -0
  43. package/server/routers/__init__.py +32 -0
  44. package/server/routers/agent.py +177 -0
  45. package/server/routers/assistant_chat.py +327 -0
  46. package/server/routers/devserver.py +309 -0
  47. package/server/routers/expand_project.py +239 -0
  48. package/server/routers/features.py +746 -0
  49. package/server/routers/filesystem.py +514 -0
  50. package/server/routers/projects.py +524 -0
  51. package/server/routers/schedules.py +356 -0
  52. package/server/routers/settings.py +127 -0
  53. package/server/routers/spec_creation.py +357 -0
  54. package/server/routers/terminal.py +453 -0
  55. package/server/schemas.py +593 -0
  56. package/server/services/__init__.py +36 -0
  57. package/server/services/assistant_chat_session.py +496 -0
  58. package/server/services/assistant_database.py +304 -0
  59. package/server/services/chat_constants.py +57 -0
  60. package/server/services/dev_server_manager.py +557 -0
  61. package/server/services/expand_chat_session.py +399 -0
  62. package/server/services/process_manager.py +657 -0
  63. package/server/services/project_config.py +475 -0
  64. package/server/services/scheduler_service.py +683 -0
  65. package/server/services/spec_chat_session.py +502 -0
  66. package/server/services/terminal_manager.py +756 -0
  67. package/server/utils/__init__.py +1 -0
  68. package/server/utils/process_utils.py +134 -0
  69. package/server/utils/project_helpers.py +32 -0
  70. package/server/utils/validation.py +54 -0
  71. package/server/websocket.py +903 -0
  72. package/start.py +456 -0
  73. package/ui/dist/assets/index-8W_wmZzz.js +168 -0
  74. package/ui/dist/assets/index-B47Ubhox.css +1 -0
  75. package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
  76. package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
  77. package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
  78. package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
  79. package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
  80. package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
  81. package/ui/dist/index.html +23 -0
  82. package/ui/dist/ollama.png +0 -0
  83. package/ui/dist/vite.svg +6 -0
  84. 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!")
@@ -0,0 +1,10 @@
1
+ """
2
+ API Package
3
+ ============
4
+
5
+ Database models and utilities for feature management.
6
+ """
7
+
8
+ from api.database import Feature, create_database, get_database_path
9
+
10
+ __all__ = ["Feature", "create_database", "get_database_path"]