autoforge-ai 0.1.20 → 0.1.22

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.
@@ -1,280 +1,280 @@
1
- """
2
- Browser View Service
3
- ====================
4
-
5
- Captures periodic screenshots from active playwright-cli browser sessions
6
- and streams them to the UI via WebSocket callbacks.
7
-
8
- Each agent gets an isolated browser session (e.g., coding-5, testing-0).
9
- This service polls those sessions with `playwright-cli screenshot` and
10
- delivers the frames to subscribed UI clients.
11
- """
12
-
13
- import asyncio
14
- import base64
15
- import logging
16
- import shutil
17
- import threading
18
- from dataclasses import dataclass
19
- from datetime import datetime
20
- from pathlib import Path
21
- from typing import Awaitable, Callable
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- POLL_INTERVAL = 2.0 # seconds between screenshot captures
26
- BACKOFF_INTERVAL = 10.0 # seconds after repeated failures
27
- MAX_FAILURES_BEFORE_BACKOFF = 10
28
- MAX_FAILURES_BEFORE_STOP = 90 # ~3 minutes at normal rate before giving up
29
- SCREENSHOT_TIMEOUT = 5 # seconds
30
-
31
-
32
- @dataclass
33
- class SessionInfo:
34
- """Metadata for an active browser session."""
35
- session_name: str
36
- agent_index: int
37
- agent_type: str # "coding" or "testing"
38
- feature_id: int
39
- feature_name: str
40
- consecutive_failures: int = 0
41
- stopped: bool = False
42
-
43
-
44
- @dataclass
45
- class ScreenshotData:
46
- """A captured screenshot ready for delivery."""
47
- session_name: str
48
- agent_index: int
49
- agent_type: str
50
- feature_id: int
51
- feature_name: str
52
- image_base64: str # base64-encoded PNG
53
- timestamp: str
54
-
55
-
56
- class BrowserViewService:
57
- """Manages screenshot capture for active agent browser sessions.
58
-
59
- Follows the same singleton-per-project pattern as DevServerProcessManager.
60
- """
61
-
62
- def __init__(self, project_name: str, project_dir: Path):
63
- self.project_name = project_name
64
- self.project_dir = project_dir
65
- self._active_sessions: dict[str, SessionInfo] = {}
66
- self._subscribers = 0
67
- self._poll_task: asyncio.Task | None = None
68
- self._screenshot_callbacks: set[Callable[[ScreenshotData], Awaitable[None]]] = set()
69
- self._lock = asyncio.Lock()
70
- self._playwright_cli: str | None = None
71
-
72
- def _get_playwright_cli(self) -> str | None:
73
- """Find playwright-cli executable."""
74
- if self._playwright_cli is not None:
75
- return self._playwright_cli
76
- path = shutil.which("playwright-cli")
77
- if path:
78
- self._playwright_cli = path
79
- else:
80
- logger.warning("playwright-cli not found in PATH; browser view disabled")
81
- return self._playwright_cli
82
-
83
- async def register_session(
84
- self,
85
- session_name: str,
86
- agent_index: int,
87
- agent_type: str,
88
- feature_id: int,
89
- feature_name: str,
90
- ) -> None:
91
- """Register an agent's browser session for screenshot capture."""
92
- async with self._lock:
93
- self._active_sessions[session_name] = SessionInfo(
94
- session_name=session_name,
95
- agent_index=agent_index,
96
- agent_type=agent_type,
97
- feature_id=feature_id,
98
- feature_name=feature_name,
99
- )
100
- logger.debug("Registered browser session: %s", session_name)
101
-
102
- async def unregister_session(self, session_name: str) -> None:
103
- """Unregister a browser session when agent completes."""
104
- async with self._lock:
105
- removed = self._active_sessions.pop(session_name, None)
106
- if removed:
107
- logger.debug("Unregistered browser session: %s", session_name)
108
- # Clean up screenshot file
109
- self._cleanup_screenshot_file(session_name)
110
-
111
- def add_screenshot_callback(self, callback: Callable[[ScreenshotData], Awaitable[None]]) -> None:
112
- self._screenshot_callbacks.add(callback)
113
-
114
- def remove_screenshot_callback(self, callback: Callable[[ScreenshotData], Awaitable[None]]) -> None:
115
- self._screenshot_callbacks.discard(callback)
116
-
117
- async def add_subscriber(self) -> None:
118
- """Called when a UI client wants browser screenshots."""
119
- async with self._lock:
120
- self._subscribers += 1
121
- if self._subscribers == 1:
122
- self._start_polling()
123
-
124
- async def remove_subscriber(self) -> None:
125
- """Called when a UI client stops wanting screenshots."""
126
- async with self._lock:
127
- self._subscribers = max(0, self._subscribers - 1)
128
- if self._subscribers == 0:
129
- self._stop_polling()
130
-
131
- async def stop(self) -> None:
132
- """Clean up all sessions and stop polling."""
133
- async with self._lock:
134
- for session_name in list(self._active_sessions):
135
- self._cleanup_screenshot_file(session_name)
136
- self._active_sessions.clear()
137
- self._stop_polling()
138
-
139
- def _start_polling(self) -> None:
140
- """Start the screenshot polling loop."""
141
- if self._poll_task is not None and not self._poll_task.done():
142
- return
143
- self._poll_task = asyncio.create_task(self._poll_loop())
144
- logger.info("Started browser screenshot polling for %s", self.project_name)
145
-
146
- def _stop_polling(self) -> None:
147
- """Stop the screenshot polling loop."""
148
- if self._poll_task is not None and not self._poll_task.done():
149
- self._poll_task.cancel()
150
- self._poll_task = None
151
- logger.info("Stopped browser screenshot polling for %s", self.project_name)
152
-
153
- async def _poll_loop(self) -> None:
154
- """Main polling loop - capture screenshots for all active sessions."""
155
- try:
156
- while True:
157
- async with self._lock:
158
- sessions = list(self._active_sessions.values())
159
-
160
- if sessions and self._screenshot_callbacks:
161
- # Capture screenshots with limited concurrency
162
- sem = asyncio.Semaphore(3)
163
-
164
- async def capture_with_sem(session: SessionInfo) -> None:
165
- async with sem:
166
- await self._capture_and_deliver(session)
167
-
168
- await asyncio.gather(
169
- *(capture_with_sem(s) for s in sessions if not s.stopped),
170
- return_exceptions=True,
171
- )
172
-
173
- await asyncio.sleep(POLL_INTERVAL)
174
- except asyncio.CancelledError:
175
- pass
176
- except Exception:
177
- logger.warning("Browser screenshot polling crashed", exc_info=True)
178
-
179
- async def _capture_and_deliver(self, session: SessionInfo) -> None:
180
- """Capture a screenshot for a session and deliver to callbacks."""
181
- cli = self._get_playwright_cli()
182
- if not cli:
183
- return
184
-
185
- # Determine interval based on failure count
186
- if session.consecutive_failures >= MAX_FAILURES_BEFORE_BACKOFF:
187
- # In backoff mode - only capture every BACKOFF_INTERVAL/POLL_INTERVAL polls
188
- # We achieve this by checking a simple modulo on failure count
189
- if session.consecutive_failures % int(BACKOFF_INTERVAL / POLL_INTERVAL) != 0:
190
- return
191
-
192
- screenshot_dir = self.project_dir / ".playwright-cli"
193
- screenshot_dir.mkdir(parents=True, exist_ok=True)
194
- screenshot_path = screenshot_dir / f"_view_{session.session_name}.png"
195
-
196
- try:
197
- proc = await asyncio.create_subprocess_exec(
198
- cli, "-s", session.session_name, "screenshot",
199
- f"--filename={screenshot_path}",
200
- stdout=asyncio.subprocess.PIPE,
201
- stderr=asyncio.subprocess.PIPE,
202
- cwd=str(self.project_dir),
203
- )
204
- _, stderr = await asyncio.wait_for(proc.communicate(), timeout=SCREENSHOT_TIMEOUT)
205
-
206
- if proc.returncode != 0:
207
- session.consecutive_failures += 1
208
- if session.consecutive_failures >= MAX_FAILURES_BEFORE_STOP:
209
- session.stopped = True
210
- logger.debug(
211
- "Stopped polling session %s after %d failures",
212
- session.session_name, session.consecutive_failures,
213
- )
214
- return
215
-
216
- # Read and encode the screenshot
217
- if not screenshot_path.exists():
218
- session.consecutive_failures += 1
219
- return
220
-
221
- image_bytes = screenshot_path.read_bytes()
222
- image_base64 = base64.b64encode(image_bytes).decode("ascii")
223
-
224
- # Reset failure counter on success
225
- session.consecutive_failures = 0
226
- # Re-enable if previously stopped
227
- session.stopped = False
228
-
229
- screenshot = ScreenshotData(
230
- session_name=session.session_name,
231
- agent_index=session.agent_index,
232
- agent_type=session.agent_type,
233
- feature_id=session.feature_id,
234
- feature_name=session.feature_name,
235
- image_base64=image_base64,
236
- timestamp=datetime.now().isoformat(),
237
- )
238
-
239
- # Deliver to all callbacks
240
- for callback in list(self._screenshot_callbacks):
241
- try:
242
- await callback(screenshot)
243
- except Exception:
244
- pass # Connection may be closed
245
-
246
- except asyncio.TimeoutError:
247
- session.consecutive_failures += 1
248
- except Exception:
249
- session.consecutive_failures += 1
250
- finally:
251
- # Clean up the screenshot file
252
- try:
253
- screenshot_path.unlink(missing_ok=True)
254
- except Exception:
255
- pass
256
-
257
- def _cleanup_screenshot_file(self, session_name: str) -> None:
258
- """Remove a session's screenshot file."""
259
- try:
260
- path = self.project_dir / ".playwright-cli" / f"_view_{session_name}.png"
261
- path.unlink(missing_ok=True)
262
- except Exception:
263
- pass
264
-
265
-
266
- # ---------------------------------------------------------------------------
267
- # Global instance management (thread-safe)
268
- # ---------------------------------------------------------------------------
269
-
270
- _services: dict[tuple[str, str], BrowserViewService] = {}
271
- _services_lock = threading.Lock()
272
-
273
-
274
- def get_browser_view_service(project_name: str, project_dir: Path) -> BrowserViewService:
275
- """Get or create a BrowserViewService for a project (thread-safe)."""
276
- with _services_lock:
277
- key = (project_name, str(project_dir.resolve()))
278
- if key not in _services:
279
- _services[key] = BrowserViewService(project_name, project_dir)
280
- return _services[key]
1
+ """
2
+ Browser View Service
3
+ ====================
4
+
5
+ Captures periodic screenshots from active playwright-cli browser sessions
6
+ and streams them to the UI via WebSocket callbacks.
7
+
8
+ Each agent gets an isolated browser session (e.g., coding-5, testing-0).
9
+ This service polls those sessions with `playwright-cli screenshot` and
10
+ delivers the frames to subscribed UI clients.
11
+ """
12
+
13
+ import asyncio
14
+ import base64
15
+ import logging
16
+ import shutil
17
+ import threading
18
+ from dataclasses import dataclass
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Awaitable, Callable
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ POLL_INTERVAL = 2.0 # seconds between screenshot captures
26
+ BACKOFF_INTERVAL = 10.0 # seconds after repeated failures
27
+ MAX_FAILURES_BEFORE_BACKOFF = 10
28
+ MAX_FAILURES_BEFORE_STOP = 90 # ~3 minutes at normal rate before giving up
29
+ SCREENSHOT_TIMEOUT = 5 # seconds
30
+
31
+
32
+ @dataclass
33
+ class SessionInfo:
34
+ """Metadata for an active browser session."""
35
+ session_name: str
36
+ agent_index: int
37
+ agent_type: str # "coding" or "testing"
38
+ feature_id: int
39
+ feature_name: str
40
+ consecutive_failures: int = 0
41
+ stopped: bool = False
42
+
43
+
44
+ @dataclass
45
+ class ScreenshotData:
46
+ """A captured screenshot ready for delivery."""
47
+ session_name: str
48
+ agent_index: int
49
+ agent_type: str
50
+ feature_id: int
51
+ feature_name: str
52
+ image_base64: str # base64-encoded PNG
53
+ timestamp: str
54
+
55
+
56
+ class BrowserViewService:
57
+ """Manages screenshot capture for active agent browser sessions.
58
+
59
+ Follows the same singleton-per-project pattern as DevServerProcessManager.
60
+ """
61
+
62
+ def __init__(self, project_name: str, project_dir: Path):
63
+ self.project_name = project_name
64
+ self.project_dir = project_dir
65
+ self._active_sessions: dict[str, SessionInfo] = {}
66
+ self._subscribers = 0
67
+ self._poll_task: asyncio.Task | None = None
68
+ self._screenshot_callbacks: set[Callable[[ScreenshotData], Awaitable[None]]] = set()
69
+ self._lock = asyncio.Lock()
70
+ self._playwright_cli: str | None = None
71
+
72
+ def _get_playwright_cli(self) -> str | None:
73
+ """Find playwright-cli executable."""
74
+ if self._playwright_cli is not None:
75
+ return self._playwright_cli
76
+ path = shutil.which("playwright-cli")
77
+ if path:
78
+ self._playwright_cli = path
79
+ else:
80
+ logger.warning("playwright-cli not found in PATH; browser view disabled")
81
+ return self._playwright_cli
82
+
83
+ async def register_session(
84
+ self,
85
+ session_name: str,
86
+ agent_index: int,
87
+ agent_type: str,
88
+ feature_id: int,
89
+ feature_name: str,
90
+ ) -> None:
91
+ """Register an agent's browser session for screenshot capture."""
92
+ async with self._lock:
93
+ self._active_sessions[session_name] = SessionInfo(
94
+ session_name=session_name,
95
+ agent_index=agent_index,
96
+ agent_type=agent_type,
97
+ feature_id=feature_id,
98
+ feature_name=feature_name,
99
+ )
100
+ logger.debug("Registered browser session: %s", session_name)
101
+
102
+ async def unregister_session(self, session_name: str) -> None:
103
+ """Unregister a browser session when agent completes."""
104
+ async with self._lock:
105
+ removed = self._active_sessions.pop(session_name, None)
106
+ if removed:
107
+ logger.debug("Unregistered browser session: %s", session_name)
108
+ # Clean up screenshot file
109
+ self._cleanup_screenshot_file(session_name)
110
+
111
+ def add_screenshot_callback(self, callback: Callable[[ScreenshotData], Awaitable[None]]) -> None:
112
+ self._screenshot_callbacks.add(callback)
113
+
114
+ def remove_screenshot_callback(self, callback: Callable[[ScreenshotData], Awaitable[None]]) -> None:
115
+ self._screenshot_callbacks.discard(callback)
116
+
117
+ async def add_subscriber(self) -> None:
118
+ """Called when a UI client wants browser screenshots."""
119
+ async with self._lock:
120
+ self._subscribers += 1
121
+ if self._subscribers == 1:
122
+ self._start_polling()
123
+
124
+ async def remove_subscriber(self) -> None:
125
+ """Called when a UI client stops wanting screenshots."""
126
+ async with self._lock:
127
+ self._subscribers = max(0, self._subscribers - 1)
128
+ if self._subscribers == 0:
129
+ self._stop_polling()
130
+
131
+ async def stop(self) -> None:
132
+ """Clean up all sessions and stop polling."""
133
+ async with self._lock:
134
+ for session_name in list(self._active_sessions):
135
+ self._cleanup_screenshot_file(session_name)
136
+ self._active_sessions.clear()
137
+ self._stop_polling()
138
+
139
+ def _start_polling(self) -> None:
140
+ """Start the screenshot polling loop."""
141
+ if self._poll_task is not None and not self._poll_task.done():
142
+ return
143
+ self._poll_task = asyncio.create_task(self._poll_loop())
144
+ logger.info("Started browser screenshot polling for %s", self.project_name)
145
+
146
+ def _stop_polling(self) -> None:
147
+ """Stop the screenshot polling loop."""
148
+ if self._poll_task is not None and not self._poll_task.done():
149
+ self._poll_task.cancel()
150
+ self._poll_task = None
151
+ logger.info("Stopped browser screenshot polling for %s", self.project_name)
152
+
153
+ async def _poll_loop(self) -> None:
154
+ """Main polling loop - capture screenshots for all active sessions."""
155
+ try:
156
+ while True:
157
+ async with self._lock:
158
+ sessions = list(self._active_sessions.values())
159
+
160
+ if sessions and self._screenshot_callbacks:
161
+ # Capture screenshots with limited concurrency
162
+ sem = asyncio.Semaphore(3)
163
+
164
+ async def capture_with_sem(session: SessionInfo) -> None:
165
+ async with sem:
166
+ await self._capture_and_deliver(session)
167
+
168
+ await asyncio.gather(
169
+ *(capture_with_sem(s) for s in sessions if not s.stopped),
170
+ return_exceptions=True,
171
+ )
172
+
173
+ await asyncio.sleep(POLL_INTERVAL)
174
+ except asyncio.CancelledError:
175
+ pass
176
+ except Exception:
177
+ logger.warning("Browser screenshot polling crashed", exc_info=True)
178
+
179
+ async def _capture_and_deliver(self, session: SessionInfo) -> None:
180
+ """Capture a screenshot for a session and deliver to callbacks."""
181
+ cli = self._get_playwright_cli()
182
+ if not cli:
183
+ return
184
+
185
+ # Determine interval based on failure count
186
+ if session.consecutive_failures >= MAX_FAILURES_BEFORE_BACKOFF:
187
+ # In backoff mode - only capture every BACKOFF_INTERVAL/POLL_INTERVAL polls
188
+ # We achieve this by checking a simple modulo on failure count
189
+ if session.consecutive_failures % int(BACKOFF_INTERVAL / POLL_INTERVAL) != 0:
190
+ return
191
+
192
+ screenshot_dir = self.project_dir / ".playwright-cli"
193
+ screenshot_dir.mkdir(parents=True, exist_ok=True)
194
+ screenshot_path = screenshot_dir / f"_view_{session.session_name}.png"
195
+
196
+ try:
197
+ proc = await asyncio.create_subprocess_exec(
198
+ cli, "-s", session.session_name, "screenshot",
199
+ f"--filename={screenshot_path}",
200
+ stdout=asyncio.subprocess.PIPE,
201
+ stderr=asyncio.subprocess.PIPE,
202
+ cwd=str(self.project_dir),
203
+ )
204
+ _, stderr = await asyncio.wait_for(proc.communicate(), timeout=SCREENSHOT_TIMEOUT)
205
+
206
+ if proc.returncode != 0:
207
+ session.consecutive_failures += 1
208
+ if session.consecutive_failures >= MAX_FAILURES_BEFORE_STOP:
209
+ session.stopped = True
210
+ logger.debug(
211
+ "Stopped polling session %s after %d failures",
212
+ session.session_name, session.consecutive_failures,
213
+ )
214
+ return
215
+
216
+ # Read and encode the screenshot
217
+ if not screenshot_path.exists():
218
+ session.consecutive_failures += 1
219
+ return
220
+
221
+ image_bytes = screenshot_path.read_bytes()
222
+ image_base64 = base64.b64encode(image_bytes).decode("ascii")
223
+
224
+ # Reset failure counter on success
225
+ session.consecutive_failures = 0
226
+ # Re-enable if previously stopped
227
+ session.stopped = False
228
+
229
+ screenshot = ScreenshotData(
230
+ session_name=session.session_name,
231
+ agent_index=session.agent_index,
232
+ agent_type=session.agent_type,
233
+ feature_id=session.feature_id,
234
+ feature_name=session.feature_name,
235
+ image_base64=image_base64,
236
+ timestamp=datetime.now().isoformat(),
237
+ )
238
+
239
+ # Deliver to all callbacks
240
+ for callback in list(self._screenshot_callbacks):
241
+ try:
242
+ await callback(screenshot)
243
+ except Exception:
244
+ pass # Connection may be closed
245
+
246
+ except asyncio.TimeoutError:
247
+ session.consecutive_failures += 1
248
+ except Exception:
249
+ session.consecutive_failures += 1
250
+ finally:
251
+ # Clean up the screenshot file
252
+ try:
253
+ screenshot_path.unlink(missing_ok=True)
254
+ except Exception:
255
+ pass
256
+
257
+ def _cleanup_screenshot_file(self, session_name: str) -> None:
258
+ """Remove a session's screenshot file."""
259
+ try:
260
+ path = self.project_dir / ".playwright-cli" / f"_view_{session_name}.png"
261
+ path.unlink(missing_ok=True)
262
+ except Exception:
263
+ pass
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Global instance management (thread-safe)
268
+ # ---------------------------------------------------------------------------
269
+
270
+ _services: dict[tuple[str, str], BrowserViewService] = {}
271
+ _services_lock = threading.Lock()
272
+
273
+
274
+ def get_browser_view_service(project_name: str, project_dir: Path) -> BrowserViewService:
275
+ """Get or create a BrowserViewService for a project (thread-safe)."""
276
+ with _services_lock:
277
+ key = (project_name, str(project_dir.resolve()))
278
+ if key not in _services:
279
+ _services[key] = BrowserViewService(project_name, project_dir)
280
+ return _services[key]
@@ -161,8 +161,9 @@ class ExpandChatSession:
161
161
  system_prompt = skill_content.replace("$ARGUMENTS", project_path)
162
162
 
163
163
  # Build environment overrides for API configuration
164
- from registry import DEFAULT_MODEL, get_effective_sdk_env
164
+ from registry import DEFAULT_MODEL, get_effective_sdk_env, get_effort_setting
165
165
  sdk_env = get_effective_sdk_env()
166
+ effort = get_effort_setting()
166
167
 
167
168
  # Determine model from SDK env (provider-aware) or fallback to env/default
168
169
  model = sdk_env.get("ANTHROPIC_DEFAULT_OPUS_MODEL") or os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", DEFAULT_MODEL)
@@ -184,6 +185,7 @@ class ExpandChatSession:
184
185
  self.client = ClaudeSDKClient(
185
186
  options=ClaudeAgentOptions(
186
187
  model=model,
188
+ effort=effort, # type: ignore[arg-type] # SDK 0.1.61 Literal omits "xhigh"
187
189
  cli_path=system_cli,
188
190
  system_prompt=system_prompt,
189
191
  allowed_tools=[
@@ -85,6 +85,7 @@ class AgentProcessManager:
85
85
  self.parallel_mode: bool = False # Parallel execution mode
86
86
  self.max_concurrency: int | None = None # Max concurrent agents
87
87
  self.testing_agent_ratio: int = 1 # Regression testing agents (0-3)
88
+ self.auto_improve: bool = False # Auto-improve mode (single session)
88
89
 
89
90
  # Support multiple callbacks (for multiple WebSocket clients)
90
91
  self._output_callbacks: Set[Callable[[str], Awaitable[None]]] = set()
@@ -375,6 +376,7 @@ class AgentProcessManager:
375
376
  playwright_headless: bool = True,
376
377
  batch_size: int = 3,
377
378
  testing_batch_size: int = 3,
379
+ auto_improve: bool = False,
378
380
  ) -> tuple[bool, str]:
379
381
  """
380
382
  Start the agent as a subprocess.
@@ -386,6 +388,10 @@ class AgentProcessManager:
386
388
  max_concurrency: Max concurrent coding agents (1-5, default 1)
387
389
  testing_agent_ratio: Number of regression testing agents (0-3, default 1)
388
390
  playwright_headless: If True, run browser in headless mode
391
+ auto_improve: If True, run in auto-improve mode. Forces single-agent
392
+ execution (concurrency=1, testing_ratio=0) and passes
393
+ --auto-improve to the subprocess so it runs exactly one
394
+ improvement session and exits.
389
395
 
390
396
  Returns:
391
397
  Tuple of (success, message)
@@ -408,12 +414,19 @@ class AgentProcessManager:
408
414
  # Clean up features stuck from a previous crash/stop
409
415
  self._cleanup_stale_features()
410
416
 
417
+ # Auto-improve mode forces single-agent execution and skips testing
418
+ # agents — the subprocess bypasses the orchestrator entirely.
419
+ if auto_improve:
420
+ max_concurrency = 1
421
+ testing_agent_ratio = 0
422
+
411
423
  # Store for status queries
412
424
  self.yolo_mode = yolo_mode
413
425
  self.model = model
414
426
  self.parallel_mode = True # Always True now (unified orchestrator)
415
427
  self.max_concurrency = max_concurrency or 1
416
428
  self.testing_agent_ratio = testing_agent_ratio
429
+ self.auto_improve = auto_improve
417
430
 
418
431
  # Build command - unified orchestrator with --concurrency
419
432
  cmd = [
@@ -432,6 +445,10 @@ class AgentProcessManager:
432
445
  if yolo_mode:
433
446
  cmd.append("--yolo")
434
447
 
448
+ # Add --auto-improve flag: bypasses the orchestrator for a one-shot run
449
+ if auto_improve:
450
+ cmd.append("--auto-improve")
451
+
435
452
  # Add --concurrency flag (unified orchestrator always uses this)
436
453
  cmd.extend(["--concurrency", str(max_concurrency or 1)])
437
454