autoforge-ai 0.1.15 → 0.1.17
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/autonomous_agent_demo.py +2 -2
- package/package.json +1 -1
- package/parallel_orchestrator.py +4 -4
- package/server/main.py +2 -0
- package/server/routers/__init__.py +2 -0
- package/server/routers/agent.py +11 -4
- package/server/routers/scaffold.py +136 -0
- package/server/routers/settings.py +5 -0
- package/server/schemas.py +13 -4
- package/server/services/process_manager.py +4 -0
- package/ui/dist/assets/index-CP8iLkFV.css +1 -0
- package/ui/dist/assets/index-CkQ1S0MR.js +96 -0
- package/ui/dist/assets/vendor-flow-CSXy01ye.js +7 -0
- package/ui/dist/assets/{vendor-markdown-lmnOnLXp.js → vendor-markdown-BxiGvyag.js} +3 -3
- package/ui/dist/assets/vendor-query-CcgjkJlA.js +1 -0
- package/ui/dist/assets/vendor-radix-DIVIznMB.js +45 -0
- package/ui/dist/assets/vendor-react-l0sNRNKZ.js +1 -0
- package/ui/dist/assets/vendor-utils-CJmVD20L.js +2 -0
- package/ui/dist/index.html +7 -8
- package/ui/dist/assets/index-CxuZ3Odc.js +0 -95
- package/ui/dist/assets/index-JNM7eUj7.css +0 -1
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +0 -7
- package/ui/dist/assets/vendor-query-BUABzP5o.js +0 -1
- package/ui/dist/assets/vendor-radix-DjWauVBs.js +0 -45
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +0 -1
- package/ui/dist/assets/vendor-utils-BaL7xioT.js +0 -2
package/autonomous_agent_demo.py
CHANGED
|
@@ -176,14 +176,14 @@ Authentication:
|
|
|
176
176
|
"--testing-batch-size",
|
|
177
177
|
type=int,
|
|
178
178
|
default=3,
|
|
179
|
-
help="Number of features per testing batch (1-
|
|
179
|
+
help="Number of features per testing batch (1-15, default: 3)",
|
|
180
180
|
)
|
|
181
181
|
|
|
182
182
|
parser.add_argument(
|
|
183
183
|
"--batch-size",
|
|
184
184
|
type=int,
|
|
185
185
|
default=3,
|
|
186
|
-
help="Max features per coding agent batch (1-
|
|
186
|
+
help="Max features per coding agent batch (1-15, default: 3)",
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
return parser.parse_args()
|
package/package.json
CHANGED
package/parallel_orchestrator.py
CHANGED
|
@@ -131,7 +131,7 @@ def _dump_database_state(feature_dicts: list[dict], label: str = ""):
|
|
|
131
131
|
MAX_PARALLEL_AGENTS = 5
|
|
132
132
|
MAX_TOTAL_AGENTS = 10
|
|
133
133
|
DEFAULT_CONCURRENCY = 3
|
|
134
|
-
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-
|
|
134
|
+
DEFAULT_TESTING_BATCH_SIZE = 3 # Number of features per testing batch (1-15)
|
|
135
135
|
POLL_INTERVAL = 5 # seconds between checking for ready features
|
|
136
136
|
MAX_FEATURE_RETRIES = 3 # Maximum times to retry a failed feature
|
|
137
137
|
INITIALIZER_TIMEOUT = 1800 # 30 minutes timeout for initializer
|
|
@@ -168,7 +168,7 @@ class ParallelOrchestrator:
|
|
|
168
168
|
yolo_mode: Whether to run in YOLO mode (skip testing agents entirely)
|
|
169
169
|
testing_agent_ratio: Number of regression testing agents to maintain (0-3).
|
|
170
170
|
0 = disabled, 1-3 = maintain that many testing agents running independently.
|
|
171
|
-
testing_batch_size: Number of features to include per testing session (1-
|
|
171
|
+
testing_batch_size: Number of features to include per testing session (1-15).
|
|
172
172
|
Each testing agent receives this many features to regression test.
|
|
173
173
|
on_output: Callback for agent output (feature_id, line)
|
|
174
174
|
on_status: Callback for agent status changes (feature_id, status)
|
|
@@ -178,8 +178,8 @@ class ParallelOrchestrator:
|
|
|
178
178
|
self.model = model
|
|
179
179
|
self.yolo_mode = yolo_mode
|
|
180
180
|
self.testing_agent_ratio = min(max(testing_agent_ratio, 0), 3) # Clamp 0-3
|
|
181
|
-
self.testing_batch_size = min(max(testing_batch_size, 1),
|
|
182
|
-
self.batch_size = min(max(batch_size, 1),
|
|
181
|
+
self.testing_batch_size = min(max(testing_batch_size, 1), 15) # Clamp 1-15
|
|
182
|
+
self.batch_size = min(max(batch_size, 1), 15) # Clamp 1-15
|
|
183
183
|
self.on_output = on_output
|
|
184
184
|
self.on_status = on_status
|
|
185
185
|
|
package/server/main.py
CHANGED
|
@@ -36,6 +36,7 @@ from .routers import (
|
|
|
36
36
|
features_router,
|
|
37
37
|
filesystem_router,
|
|
38
38
|
projects_router,
|
|
39
|
+
scaffold_router,
|
|
39
40
|
schedules_router,
|
|
40
41
|
settings_router,
|
|
41
42
|
spec_creation_router,
|
|
@@ -169,6 +170,7 @@ app.include_router(filesystem_router)
|
|
|
169
170
|
app.include_router(assistant_chat_router)
|
|
170
171
|
app.include_router(settings_router)
|
|
171
172
|
app.include_router(terminal_router)
|
|
173
|
+
app.include_router(scaffold_router)
|
|
172
174
|
|
|
173
175
|
|
|
174
176
|
# ============================================================================
|
|
@@ -12,6 +12,7 @@ from .expand_project import router as expand_project_router
|
|
|
12
12
|
from .features import router as features_router
|
|
13
13
|
from .filesystem import router as filesystem_router
|
|
14
14
|
from .projects import router as projects_router
|
|
15
|
+
from .scaffold import router as scaffold_router
|
|
15
16
|
from .schedules import router as schedules_router
|
|
16
17
|
from .settings import router as settings_router
|
|
17
18
|
from .spec_creation import router as spec_creation_router
|
|
@@ -29,4 +30,5 @@ __all__ = [
|
|
|
29
30
|
"assistant_chat_router",
|
|
30
31
|
"settings_router",
|
|
31
32
|
"terminal_router",
|
|
33
|
+
"scaffold_router",
|
|
32
34
|
]
|
package/server/routers/agent.py
CHANGED
|
@@ -17,11 +17,11 @@ from ..utils.project_helpers import get_project_path as _get_project_path
|
|
|
17
17
|
from ..utils.validation import validate_project_name
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
|
20
|
+
def _get_settings_defaults() -> tuple[bool, str, int, bool, int, int]:
|
|
21
21
|
"""Get defaults from global settings.
|
|
22
22
|
|
|
23
23
|
Returns:
|
|
24
|
-
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size)
|
|
24
|
+
Tuple of (yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size)
|
|
25
25
|
"""
|
|
26
26
|
import sys
|
|
27
27
|
root = Path(__file__).parent.parent.parent
|
|
@@ -47,7 +47,12 @@ def _get_settings_defaults() -> tuple[bool, str, int, bool, int]:
|
|
|
47
47
|
except (ValueError, TypeError):
|
|
48
48
|
batch_size = 3
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
try:
|
|
51
|
+
testing_batch_size = int(settings.get("testing_batch_size", "3"))
|
|
52
|
+
except (ValueError, TypeError):
|
|
53
|
+
testing_batch_size = 3
|
|
54
|
+
|
|
55
|
+
return yolo_mode, model, testing_agent_ratio, playwright_headless, batch_size, testing_batch_size
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
router = APIRouter(prefix="/api/projects/{project_name}/agent", tags=["agent"])
|
|
@@ -96,7 +101,7 @@ async def start_agent(
|
|
|
96
101
|
manager = get_project_manager(project_name)
|
|
97
102
|
|
|
98
103
|
# 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()
|
|
104
|
+
default_yolo, default_model, default_testing_ratio, playwright_headless, default_batch_size, default_testing_batch_size = _get_settings_defaults()
|
|
100
105
|
|
|
101
106
|
yolo_mode = request.yolo_mode if request.yolo_mode is not None else default_yolo
|
|
102
107
|
model = request.model if request.model else default_model
|
|
@@ -104,6 +109,7 @@ async def start_agent(
|
|
|
104
109
|
testing_agent_ratio = request.testing_agent_ratio if request.testing_agent_ratio is not None else default_testing_ratio
|
|
105
110
|
|
|
106
111
|
batch_size = default_batch_size
|
|
112
|
+
testing_batch_size = default_testing_batch_size
|
|
107
113
|
|
|
108
114
|
success, message = await manager.start(
|
|
109
115
|
yolo_mode=yolo_mode,
|
|
@@ -112,6 +118,7 @@ async def start_agent(
|
|
|
112
118
|
testing_agent_ratio=testing_agent_ratio,
|
|
113
119
|
playwright_headless=playwright_headless,
|
|
114
120
|
batch_size=batch_size,
|
|
121
|
+
testing_batch_size=testing_batch_size,
|
|
115
122
|
)
|
|
116
123
|
|
|
117
124
|
# Notify scheduler of manual start (to prevent auto-stop during scheduled window)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scaffold Router
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
SSE streaming endpoint for running project scaffold commands.
|
|
6
|
+
Supports templated project creation (e.g., Next.js agentic starter).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Request
|
|
18
|
+
from fastapi.responses import StreamingResponse
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from .filesystem import is_path_blocked
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
router = APIRouter(prefix="/api/scaffold", tags=["scaffold"])
|
|
26
|
+
|
|
27
|
+
# Hardcoded templates — no arbitrary commands allowed
|
|
28
|
+
TEMPLATES: dict[str, list[str]] = {
|
|
29
|
+
"agentic-starter": ["npx", "create-agentic-app@latest", ".", "-y", "-p", "npm", "--skip-git"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ScaffoldRequest(BaseModel):
|
|
34
|
+
template: str
|
|
35
|
+
target_path: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _sse_event(data: dict) -> str:
|
|
39
|
+
"""Format a dict as an SSE data line."""
|
|
40
|
+
return f"data: {json.dumps(data)}\n\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _stream_scaffold(template: str, target_path: str, request: Request):
|
|
44
|
+
"""Run the scaffold command and yield SSE events."""
|
|
45
|
+
# Validate template
|
|
46
|
+
if template not in TEMPLATES:
|
|
47
|
+
yield _sse_event({"type": "error", "message": f"Unknown template: {template}"})
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# Validate path
|
|
51
|
+
path = Path(target_path)
|
|
52
|
+
try:
|
|
53
|
+
path = path.resolve()
|
|
54
|
+
except (OSError, ValueError) as e:
|
|
55
|
+
yield _sse_event({"type": "error", "message": f"Invalid path: {e}"})
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if is_path_blocked(path):
|
|
59
|
+
yield _sse_event({"type": "error", "message": "Access to this directory is not allowed"})
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if not path.exists() or not path.is_dir():
|
|
63
|
+
yield _sse_event({"type": "error", "message": "Target directory does not exist"})
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Check npx is available
|
|
67
|
+
npx_name = "npx"
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
npx_name = "npx.cmd"
|
|
70
|
+
|
|
71
|
+
if not shutil.which(npx_name):
|
|
72
|
+
yield _sse_event({"type": "error", "message": "npx is not available. Please install Node.js."})
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Build command
|
|
76
|
+
argv = list(TEMPLATES[template])
|
|
77
|
+
if sys.platform == "win32" and not argv[0].lower().endswith(".cmd"):
|
|
78
|
+
argv[0] = argv[0] + ".cmd"
|
|
79
|
+
|
|
80
|
+
process = None
|
|
81
|
+
try:
|
|
82
|
+
popen_kwargs: dict = {
|
|
83
|
+
"stdout": subprocess.PIPE,
|
|
84
|
+
"stderr": subprocess.STDOUT,
|
|
85
|
+
"stdin": subprocess.DEVNULL,
|
|
86
|
+
"cwd": str(path),
|
|
87
|
+
}
|
|
88
|
+
if sys.platform == "win32":
|
|
89
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
90
|
+
|
|
91
|
+
process = subprocess.Popen(argv, **popen_kwargs)
|
|
92
|
+
logger.info("Scaffold process started: pid=%s, template=%s, path=%s", process.pid, template, target_path)
|
|
93
|
+
|
|
94
|
+
# Stream stdout lines
|
|
95
|
+
assert process.stdout is not None
|
|
96
|
+
for raw_line in iter(process.stdout.readline, b""):
|
|
97
|
+
# Check if client disconnected
|
|
98
|
+
if await request.is_disconnected():
|
|
99
|
+
logger.info("Client disconnected during scaffold, terminating process")
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
line = raw_line.decode("utf-8", errors="replace").rstrip("\n\r")
|
|
103
|
+
yield _sse_event({"type": "output", "line": line})
|
|
104
|
+
# Yield control to event loop so disconnect checks work
|
|
105
|
+
await asyncio.sleep(0)
|
|
106
|
+
|
|
107
|
+
process.wait()
|
|
108
|
+
exit_code = process.returncode
|
|
109
|
+
success = exit_code == 0
|
|
110
|
+
logger.info("Scaffold process completed: exit_code=%s, template=%s", exit_code, template)
|
|
111
|
+
yield _sse_event({"type": "complete", "success": success, "exit_code": exit_code})
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error("Scaffold error: %s", e)
|
|
115
|
+
yield _sse_event({"type": "error", "message": str(e)})
|
|
116
|
+
|
|
117
|
+
finally:
|
|
118
|
+
if process and process.poll() is None:
|
|
119
|
+
try:
|
|
120
|
+
process.terminate()
|
|
121
|
+
process.wait(timeout=5)
|
|
122
|
+
except Exception:
|
|
123
|
+
process.kill()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@router.post("/run")
|
|
127
|
+
async def run_scaffold(body: ScaffoldRequest, request: Request):
|
|
128
|
+
"""Run a scaffold template command with SSE streaming output."""
|
|
129
|
+
return StreamingResponse(
|
|
130
|
+
_stream_scaffold(body.template, body.target_path, request),
|
|
131
|
+
media_type="text/event-stream",
|
|
132
|
+
headers={
|
|
133
|
+
"Cache-Control": "no-cache",
|
|
134
|
+
"X-Accel-Buffering": "no",
|
|
135
|
+
},
|
|
136
|
+
)
|
|
@@ -113,6 +113,7 @@ async def get_settings():
|
|
|
113
113
|
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
|
114
114
|
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
|
115
115
|
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
|
116
|
+
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
|
116
117
|
api_provider=api_provider,
|
|
117
118
|
api_base_url=all_settings.get("api_base_url"),
|
|
118
119
|
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
|
@@ -138,6 +139,9 @@ async def update_settings(update: SettingsUpdate):
|
|
|
138
139
|
if update.batch_size is not None:
|
|
139
140
|
set_setting("batch_size", str(update.batch_size))
|
|
140
141
|
|
|
142
|
+
if update.testing_batch_size is not None:
|
|
143
|
+
set_setting("testing_batch_size", str(update.testing_batch_size))
|
|
144
|
+
|
|
141
145
|
# API provider settings
|
|
142
146
|
if update.api_provider is not None:
|
|
143
147
|
old_provider = get_setting("api_provider", "claude")
|
|
@@ -177,6 +181,7 @@ async def update_settings(update: SettingsUpdate):
|
|
|
177
181
|
testing_agent_ratio=_parse_int(all_settings.get("testing_agent_ratio"), 1),
|
|
178
182
|
playwright_headless=_parse_bool(all_settings.get("playwright_headless"), default=True),
|
|
179
183
|
batch_size=_parse_int(all_settings.get("batch_size"), 3),
|
|
184
|
+
testing_batch_size=_parse_int(all_settings.get("testing_batch_size"), 3),
|
|
180
185
|
api_provider=api_provider,
|
|
181
186
|
api_base_url=all_settings.get("api_base_url"),
|
|
182
187
|
api_has_auth_token=bool(all_settings.get("api_auth_token")),
|
package/server/schemas.py
CHANGED
|
@@ -444,7 +444,8 @@ class SettingsResponse(BaseModel):
|
|
|
444
444
|
ollama_mode: bool = False # True when api_provider is "ollama"
|
|
445
445
|
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
|
446
446
|
playwright_headless: bool = True
|
|
447
|
-
batch_size: int = 3 # Features per coding agent batch (1-
|
|
447
|
+
batch_size: int = 3 # Features per coding agent batch (1-15)
|
|
448
|
+
testing_batch_size: int = 3 # Features per testing agent batch (1-15)
|
|
448
449
|
api_provider: str = "claude"
|
|
449
450
|
api_base_url: str | None = None
|
|
450
451
|
api_has_auth_token: bool = False # Never expose actual token
|
|
@@ -463,7 +464,8 @@ class SettingsUpdate(BaseModel):
|
|
|
463
464
|
model: str | None = None
|
|
464
465
|
testing_agent_ratio: int | None = None # 0-3
|
|
465
466
|
playwright_headless: bool | None = None
|
|
466
|
-
batch_size: int | None = None # Features per agent batch (1-
|
|
467
|
+
batch_size: int | None = None # Features per agent batch (1-15)
|
|
468
|
+
testing_batch_size: int | None = None # Features per testing agent batch (1-15)
|
|
467
469
|
api_provider: str | None = None
|
|
468
470
|
api_base_url: str | None = Field(None, max_length=500)
|
|
469
471
|
api_auth_token: str | None = Field(None, max_length=500) # Write-only, never returned
|
|
@@ -500,8 +502,15 @@ class SettingsUpdate(BaseModel):
|
|
|
500
502
|
@field_validator('batch_size')
|
|
501
503
|
@classmethod
|
|
502
504
|
def validate_batch_size(cls, v: int | None) -> int | None:
|
|
503
|
-
if v is not None and (v < 1 or v >
|
|
504
|
-
raise ValueError("batch_size must be between 1 and
|
|
505
|
+
if v is not None and (v < 1 or v > 15):
|
|
506
|
+
raise ValueError("batch_size must be between 1 and 15")
|
|
507
|
+
return v
|
|
508
|
+
|
|
509
|
+
@field_validator('testing_batch_size')
|
|
510
|
+
@classmethod
|
|
511
|
+
def validate_testing_batch_size(cls, v: int | None) -> int | None:
|
|
512
|
+
if v is not None and (v < 1 or v > 15):
|
|
513
|
+
raise ValueError("testing_batch_size must be between 1 and 15")
|
|
505
514
|
return v
|
|
506
515
|
|
|
507
516
|
|
|
@@ -374,6 +374,7 @@ class AgentProcessManager:
|
|
|
374
374
|
testing_agent_ratio: int = 1,
|
|
375
375
|
playwright_headless: bool = True,
|
|
376
376
|
batch_size: int = 3,
|
|
377
|
+
testing_batch_size: int = 3,
|
|
377
378
|
) -> tuple[bool, str]:
|
|
378
379
|
"""
|
|
379
380
|
Start the agent as a subprocess.
|
|
@@ -440,6 +441,9 @@ class AgentProcessManager:
|
|
|
440
441
|
# Add --batch-size flag for multi-feature batching
|
|
441
442
|
cmd.extend(["--batch-size", str(batch_size)])
|
|
442
443
|
|
|
444
|
+
# Add --testing-batch-size flag for testing agent batching
|
|
445
|
+
cmd.extend(["--testing-batch-size", str(testing_batch_size)])
|
|
446
|
+
|
|
443
447
|
# Apply headless setting to .playwright/cli.config.json so playwright-cli
|
|
444
448
|
# picks it up (the only mechanism it supports for headless control)
|
|
445
449
|
self._apply_playwright_headless(playwright_headless)
|