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
@@ -0,0 +1,17 @@
1
+ """
2
+ FastAPI Backend Server
3
+ ======================
4
+
5
+ Web UI server for the Autonomous Coding Agent.
6
+ Provides REST API and WebSocket endpoints for project management,
7
+ feature tracking, and agent control.
8
+ """
9
+
10
+ # Fix Windows asyncio subprocess support - MUST be before any other imports
11
+ # that might create an event loop
12
+ import sys
13
+
14
+ if sys.platform == "win32":
15
+ import asyncio
16
+
17
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
package/server/main.py ADDED
@@ -0,0 +1,261 @@
1
+ """
2
+ FastAPI Main Application
3
+ ========================
4
+
5
+ Main entry point for the Autonomous Coding UI server.
6
+ Provides REST API, WebSocket, and static file serving.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import shutil
13
+ import sys
14
+ from contextlib import asynccontextmanager
15
+ from pathlib import Path
16
+
17
+ # Fix for Windows subprocess support in asyncio
18
+ if sys.platform == "win32":
19
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
20
+
21
+ from dotenv import load_dotenv
22
+
23
+ # Load environment variables from .env file if present
24
+ load_dotenv()
25
+
26
+ from fastapi import FastAPI, HTTPException, Request, WebSocket
27
+ from fastapi.middleware.cors import CORSMiddleware
28
+ from fastapi.responses import FileResponse
29
+ from fastapi.staticfiles import StaticFiles
30
+
31
+ from .routers import (
32
+ agent_router,
33
+ assistant_chat_router,
34
+ devserver_router,
35
+ expand_project_router,
36
+ features_router,
37
+ filesystem_router,
38
+ projects_router,
39
+ schedules_router,
40
+ settings_router,
41
+ spec_creation_router,
42
+ terminal_router,
43
+ )
44
+ from .schemas import SetupStatus
45
+ from .services.assistant_chat_session import cleanup_all_sessions as cleanup_assistant_sessions
46
+ from .services.chat_constants import ROOT_DIR
47
+ from .services.dev_server_manager import (
48
+ cleanup_all_devservers,
49
+ cleanup_orphaned_devserver_locks,
50
+ )
51
+ from .services.expand_chat_session import cleanup_all_expand_sessions
52
+ from .services.process_manager import cleanup_all_managers, cleanup_orphaned_locks
53
+ from .services.scheduler_service import cleanup_scheduler, get_scheduler
54
+ from .services.terminal_manager import cleanup_all_terminals
55
+ from .websocket import project_websocket
56
+
57
+ # Paths
58
+ UI_DIST_DIR = ROOT_DIR / "ui" / "dist"
59
+
60
+
61
+ @asynccontextmanager
62
+ async def lifespan(app: FastAPI):
63
+ """Lifespan context manager for startup and shutdown."""
64
+ # Startup - clean up orphaned lock files from previous runs
65
+ cleanup_orphaned_locks()
66
+ cleanup_orphaned_devserver_locks()
67
+
68
+ # Start the scheduler service
69
+ scheduler = get_scheduler()
70
+ await scheduler.start()
71
+
72
+ yield
73
+
74
+ # Shutdown - cleanup scheduler first to stop triggering new starts
75
+ await cleanup_scheduler()
76
+ # Then cleanup all running agents, sessions, terminals, and dev servers
77
+ await cleanup_all_managers()
78
+ await cleanup_assistant_sessions()
79
+ await cleanup_all_expand_sessions()
80
+ await cleanup_all_terminals()
81
+ await cleanup_all_devservers()
82
+
83
+
84
+ # Create FastAPI app
85
+ app = FastAPI(
86
+ title="Autonomous Coding UI",
87
+ description="Web UI for the Autonomous Coding Agent",
88
+ version="1.0.0",
89
+ lifespan=lifespan,
90
+ )
91
+
92
+ # Module logger
93
+ logger = logging.getLogger(__name__)
94
+
95
+ # Check if remote access is enabled via environment variable
96
+ # Set by start_ui.py when --host is not 127.0.0.1
97
+ ALLOW_REMOTE = os.environ.get("AUTOFORGE_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
98
+
99
+ if ALLOW_REMOTE:
100
+ logger.warning(
101
+ "ALLOW_REMOTE is enabled. Terminal WebSocket is exposed without sandboxing. "
102
+ "Only use this in trusted network environments."
103
+ )
104
+
105
+ # CORS - allow all origins when remote access is enabled, otherwise localhost only
106
+ if ALLOW_REMOTE:
107
+ app.add_middleware(
108
+ CORSMiddleware,
109
+ allow_origins=["*"], # Allow all origins for remote access
110
+ allow_credentials=True,
111
+ allow_methods=["*"],
112
+ allow_headers=["*"],
113
+ )
114
+ else:
115
+ app.add_middleware(
116
+ CORSMiddleware,
117
+ allow_origins=[
118
+ "http://localhost:5173", # Vite dev server
119
+ "http://127.0.0.1:5173",
120
+ "http://localhost:8888", # Production
121
+ "http://127.0.0.1:8888",
122
+ ],
123
+ allow_credentials=True,
124
+ allow_methods=["*"],
125
+ allow_headers=["*"],
126
+ )
127
+
128
+
129
+ # ============================================================================
130
+ # Security Middleware
131
+ # ============================================================================
132
+
133
+ if not ALLOW_REMOTE:
134
+ @app.middleware("http")
135
+ async def require_localhost(request: Request, call_next):
136
+ """Only allow requests from localhost (disabled when AUTOFORGE_ALLOW_REMOTE=1)."""
137
+ client_host = request.client.host if request.client else None
138
+
139
+ # Allow localhost connections
140
+ if client_host not in ("127.0.0.1", "::1", "localhost", None):
141
+ raise HTTPException(status_code=403, detail="Localhost access only")
142
+
143
+ return await call_next(request)
144
+
145
+
146
+ # ============================================================================
147
+ # Include Routers
148
+ # ============================================================================
149
+
150
+ app.include_router(projects_router)
151
+ app.include_router(features_router)
152
+ app.include_router(agent_router)
153
+ app.include_router(schedules_router)
154
+ app.include_router(devserver_router)
155
+ app.include_router(spec_creation_router)
156
+ app.include_router(expand_project_router)
157
+ app.include_router(filesystem_router)
158
+ app.include_router(assistant_chat_router)
159
+ app.include_router(settings_router)
160
+ app.include_router(terminal_router)
161
+
162
+
163
+ # ============================================================================
164
+ # WebSocket Endpoint
165
+ # ============================================================================
166
+
167
+ @app.websocket("/ws/projects/{project_name}")
168
+ async def websocket_endpoint(websocket: WebSocket, project_name: str):
169
+ """WebSocket endpoint for real-time project updates."""
170
+ await project_websocket(websocket, project_name)
171
+
172
+
173
+ # ============================================================================
174
+ # Setup & Health Endpoints
175
+ # ============================================================================
176
+
177
+ @app.get("/api/health")
178
+ async def health_check():
179
+ """Health check endpoint."""
180
+ return {"status": "healthy"}
181
+
182
+
183
+ @app.get("/api/setup/status", response_model=SetupStatus)
184
+ async def setup_status():
185
+ """Check system setup status."""
186
+ # Check for Claude CLI
187
+ claude_cli = shutil.which("claude") is not None
188
+
189
+ # Check for CLI configuration directory
190
+ # Note: CLI no longer stores credentials in ~/.claude/.credentials.json
191
+ # The existence of ~/.claude indicates the CLI has been configured
192
+ claude_dir = Path.home() / ".claude"
193
+ has_claude_config = claude_dir.exists() and claude_dir.is_dir()
194
+
195
+ # If GLM mode is configured via .env, we have alternative credentials
196
+ glm_configured = bool(os.getenv("ANTHROPIC_BASE_URL") and os.getenv("ANTHROPIC_AUTH_TOKEN"))
197
+ credentials = has_claude_config or glm_configured
198
+
199
+ # Check for Node.js and npm
200
+ node = shutil.which("node") is not None
201
+ npm = shutil.which("npm") is not None
202
+
203
+ return SetupStatus(
204
+ claude_cli=claude_cli,
205
+ credentials=credentials,
206
+ node=node,
207
+ npm=npm,
208
+ )
209
+
210
+
211
+ # ============================================================================
212
+ # Static File Serving (Production)
213
+ # ============================================================================
214
+
215
+ # Serve React build files if they exist
216
+ if UI_DIST_DIR.exists():
217
+ # Mount static assets
218
+ app.mount("/assets", StaticFiles(directory=UI_DIST_DIR / "assets"), name="assets")
219
+
220
+ @app.get("/")
221
+ async def serve_index():
222
+ """Serve the React app index.html."""
223
+ return FileResponse(UI_DIST_DIR / "index.html")
224
+
225
+ @app.get("/{path:path}")
226
+ async def serve_spa(path: str):
227
+ """
228
+ Serve static files or fall back to index.html for SPA routing.
229
+ """
230
+ # Check if the path is an API route (shouldn't hit this due to router ordering)
231
+ if path.startswith("api/") or path.startswith("ws/"):
232
+ raise HTTPException(status_code=404)
233
+
234
+ # Try to serve the file directly
235
+ file_path = (UI_DIST_DIR / path).resolve()
236
+
237
+ # Ensure resolved path is within UI_DIST_DIR (prevent path traversal)
238
+ try:
239
+ file_path.relative_to(UI_DIST_DIR.resolve())
240
+ except ValueError:
241
+ raise HTTPException(status_code=404)
242
+
243
+ if file_path.exists() and file_path.is_file():
244
+ return FileResponse(file_path)
245
+
246
+ # Fall back to index.html for SPA routing
247
+ return FileResponse(UI_DIST_DIR / "index.html")
248
+
249
+
250
+ # ============================================================================
251
+ # Main Entry Point
252
+ # ============================================================================
253
+
254
+ if __name__ == "__main__":
255
+ import uvicorn
256
+ uvicorn.run(
257
+ "server.main:app",
258
+ host="127.0.0.1", # Localhost only for security
259
+ port=8888,
260
+ reload=True,
261
+ )
@@ -0,0 +1,32 @@
1
+ """
2
+ API Routers
3
+ ===========
4
+
5
+ FastAPI routers for different API endpoints.
6
+ """
7
+
8
+ from .agent import router as agent_router
9
+ from .assistant_chat import router as assistant_chat_router
10
+ from .devserver import router as devserver_router
11
+ from .expand_project import router as expand_project_router
12
+ from .features import router as features_router
13
+ from .filesystem import router as filesystem_router
14
+ from .projects import router as projects_router
15
+ from .schedules import router as schedules_router
16
+ from .settings import router as settings_router
17
+ from .spec_creation import router as spec_creation_router
18
+ from .terminal import router as terminal_router
19
+
20
+ __all__ = [
21
+ "projects_router",
22
+ "features_router",
23
+ "agent_router",
24
+ "schedules_router",
25
+ "devserver_router",
26
+ "spec_creation_router",
27
+ "expand_project_router",
28
+ "filesystem_router",
29
+ "assistant_chat_router",
30
+ "settings_router",
31
+ "terminal_router",
32
+ ]
@@ -0,0 +1,177 @@
1
+ """
2
+ Agent Router
3
+ ============
4
+
5
+ API endpoints for agent control (start/stop/pause/resume).
6
+ Uses project registry for path lookups.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from fastapi import APIRouter, HTTPException
12
+
13
+ from ..schemas import AgentActionResponse, AgentStartRequest, AgentStatus
14
+ from ..services.chat_constants import ROOT_DIR
15
+ from ..services.process_manager import get_manager
16
+ from ..utils.project_helpers import get_project_path as _get_project_path
17
+ from ..utils.validation import validate_project_name
18
+
19
+
20
+ def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
21
+ """Get defaults from global settings.
22
+
23
+ Returns:
24
+ Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size)
25
+ """
26
+ import sys
27
+ root = Path(__file__).parent.parent.parent
28
+ if str(root) not in sys.path:
29
+ sys.path.insert(0, str(root))
30
+
31
+ from registry import DEFAULT_MODEL, get_all_settings
32
+
33
+ settings = get_all_settings()
34
+ yolo_mode = (settings.get("yolo_mode") or "false").lower() == "true"
35
+ model = settings.get("model", DEFAULT_MODEL)
36
+
37
+ # Parse testing agent settings with defaults
38
+ try:
39
+ testing_agent_ratio = int(settings.get("testing_agent_ratio", "1"))
40
+ except (ValueError, TypeError):
41
+ testing_agent_ratio = 1
42
+
43
+ playwright_headless = (settings.get("playwright_headless") or "true").lower() == "true"
44
+
45
+ try:
46
+ batch_size = int(settings.get("batch_size", "3"))
47
+ except (ValueError, TypeError):
48
+ batch_size = 3
49
+
50
+ return yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size
51
+
52
+
53
+ router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
54
+
55
+
56
+ def get_project_manager(project_name: str):
57
+ """Get the process manager for a project."""
58
+ project_name = validate_project_name(project_name)
59
+ project_dir = _get_project_path(project_name)
60
+
61
+ if not project_dir:
62
+ raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found in registry")
63
+
64
+ if not project_dir.exists():
65
+ raise HTTPException(status_code=404, detail=f"Project directory not found: {project_dir}")
66
+
67
+ return get_manager(project_name, project_dir, ROOT_DIR)
68
+
69
+
70
+ @router.get("/status", response_model=AgentStatus)
71
+ async def get_agent_status(project_name: str):
72
+ """Get the current status of the agent for a project."""
73
+ manager = get_project_manager(project_name)
74
+
75
+ # Run healthcheck to detect crashed processes
76
+ await manager.healthcheck()
77
+
78
+ return AgentStatus(
79
+ status=manager.status,
80
+ pid=manager.pid,
81
+ started_at=manager.started_at.isoformat() if manager.started_at else None,
82
+ yolo_mode=manager.yolo_mode,
83
+ model=manager.model,
84
+ parallel_mode=manager.parallel_mode,
85
+ max_concurrency=manager.max_concurrency,
86
+ testing_agent_ratio=manager.testing_agent_ratio,
87
+ )
88
+
89
+
90
+ @router.post("/start", response_model=AgentActionResponse)
91
+ async def start_agent(
92
+ project_name: str,
93
+ request: AgentStartRequest = AgentStartRequest(),
94
+ ):
95
+ """Start the agent for a project."""
96
+ manager = get_project_manager(project_name)
97
+
98
+ # Get defaults from global settings if not provided in request
99
+ default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size = _get_settings_defaults()
100
+
101
+ yolo_mode = request.yolo_mode if request.yolo_mode is not None else default_yolo
102
+ model = request.model if request.model else default_model
103
+ max_concurrency = request.max_concurrency or 1
104
+ testing_agent_ratio = request.testing_agent_ratio if request.testing_agent_ratio is not None else default_testing_ratio
105
+
106
+ batch_size = default_batch_size
107
+
108
+ success, message = await manager.start(
109
+ yolo_mode=yolo_mode,
110
+ model=model,
111
+ max_concurrency=max_concurrency,
112
+ testing_agent_ratio=testing_agent_ratio,
113
+ playwright_headless=playwright_headless,
114
+ batch_size=batch_size,
115
+ )
116
+
117
+ # Notify scheduler of manual start (to prevent auto-stop during scheduled window)
118
+ if success:
119
+ from ..services.scheduler_service import get_scheduler
120
+ project_dir = _get_project_path(project_name)
121
+ if project_dir:
122
+ get_scheduler().notify_manual_start(project_name, project_dir)
123
+
124
+ return AgentActionResponse(
125
+ success=success,
126
+ status=manager.status,
127
+ message=message,
128
+ )
129
+
130
+
131
+ @router.post("/stop", response_model=AgentActionResponse)
132
+ async def stop_agent(project_name: str):
133
+ """Stop the agent for a project."""
134
+ manager = get_project_manager(project_name)
135
+
136
+ success, message = await manager.stop()
137
+
138
+ # Notify scheduler of manual stop (to prevent auto-start during scheduled window)
139
+ if success:
140
+ from ..services.scheduler_service import get_scheduler
141
+ project_dir = _get_project_path(project_name)
142
+ if project_dir:
143
+ get_scheduler().notify_manual_stop(project_name, project_dir)
144
+
145
+ return AgentActionResponse(
146
+ success=success,
147
+ status=manager.status,
148
+ message=message,
149
+ )
150
+
151
+
152
+ @router.post("/pause", response_model=AgentActionResponse)
153
+ async def pause_agent(project_name: str):
154
+ """Pause the agent for a project."""
155
+ manager = get_project_manager(project_name)
156
+
157
+ success, message = await manager.pause()
158
+
159
+ return AgentActionResponse(
160
+ success=success,
161
+ status=manager.status,
162
+ message=message,
163
+ )
164
+
165
+
166
+ @router.post("/resume", response_model=AgentActionResponse)
167
+ async def resume_agent(project_name: str):
168
+ """Resume a paused agent."""
169
+ manager = get_project_manager(project_name)
170
+
171
+ success, message = await manager.resume()
172
+
173
+ return AgentActionResponse(
174
+ success=success,
175
+ status=manager.status,
176
+ message=message,
177
+ )