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
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic Schemas
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
Request/Response models for the API endpoints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field, field_validator
|
|
15
|
+
|
|
16
|
+
# Import model constants from registry (single source of truth)
|
|
17
|
+
_root = Path(__file__).parent.parent
|
|
18
|
+
if str(_root) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(_root))
|
|
20
|
+
|
|
21
|
+
from registry import DEFAULT_MODEL, VALID_MODELS
|
|
22
|
+
|
|
23
|
+
# ============================================================================
|
|
24
|
+
# Project Schemas
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
class ProjectCreate(BaseModel):
|
|
28
|
+
"""Request schema for creating a new project."""
|
|
29
|
+
name: str = Field(..., min_length=1, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
|
|
30
|
+
path: str = Field(..., min_length=1, description="Absolute path to project directory")
|
|
31
|
+
spec_method: Literal["claude", "manual"] = "claude"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProjectStats(BaseModel):
|
|
35
|
+
"""Project statistics."""
|
|
36
|
+
passing: int = 0
|
|
37
|
+
in_progress: int = 0
|
|
38
|
+
total: int = 0
|
|
39
|
+
percentage: float = 0.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ProjectSummary(BaseModel):
|
|
43
|
+
"""Summary of a project for list view."""
|
|
44
|
+
name: str
|
|
45
|
+
path: str
|
|
46
|
+
has_spec: bool
|
|
47
|
+
stats: ProjectStats
|
|
48
|
+
default_concurrency: int = 3
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProjectDetail(BaseModel):
|
|
52
|
+
"""Detailed project information."""
|
|
53
|
+
name: str
|
|
54
|
+
path: str
|
|
55
|
+
has_spec: bool
|
|
56
|
+
stats: ProjectStats
|
|
57
|
+
prompts_dir: str
|
|
58
|
+
default_concurrency: int = 3
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ProjectPrompts(BaseModel):
|
|
62
|
+
"""Project prompt files content."""
|
|
63
|
+
app_spec: str = ""
|
|
64
|
+
initializer_prompt: str = ""
|
|
65
|
+
coding_prompt: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProjectPromptsUpdate(BaseModel):
|
|
69
|
+
"""Request schema for updating project prompts."""
|
|
70
|
+
app_spec: str | None = None
|
|
71
|
+
initializer_prompt: str | None = None
|
|
72
|
+
coding_prompt: str | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ProjectSettingsUpdate(BaseModel):
|
|
76
|
+
"""Request schema for updating project-level settings."""
|
|
77
|
+
default_concurrency: int | None = None
|
|
78
|
+
|
|
79
|
+
@field_validator('default_concurrency')
|
|
80
|
+
@classmethod
|
|
81
|
+
def validate_concurrency(cls, v: int | None) -> int | None:
|
|
82
|
+
if v is not None and (v < 1 or v > 5):
|
|
83
|
+
raise ValueError("default_concurrency must be between 1 and 5")
|
|
84
|
+
return v
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ============================================================================
|
|
88
|
+
# Feature Schemas
|
|
89
|
+
# ============================================================================
|
|
90
|
+
|
|
91
|
+
class FeatureBase(BaseModel):
|
|
92
|
+
"""Base feature attributes."""
|
|
93
|
+
category: str
|
|
94
|
+
name: str
|
|
95
|
+
description: str
|
|
96
|
+
steps: list[str]
|
|
97
|
+
dependencies: list[int] = Field(default_factory=list) # Optional dependencies
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FeatureCreate(FeatureBase):
|
|
101
|
+
"""Request schema for creating a new feature."""
|
|
102
|
+
priority: int | None = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FeatureUpdate(BaseModel):
|
|
106
|
+
"""Request schema for updating a feature (partial updates allowed)."""
|
|
107
|
+
category: str | None = None
|
|
108
|
+
name: str | None = None
|
|
109
|
+
description: str | None = None
|
|
110
|
+
steps: list[str] | None = None
|
|
111
|
+
priority: int | None = None
|
|
112
|
+
dependencies: list[int] | None = None # Optional - can update dependencies
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FeatureResponse(FeatureBase):
|
|
116
|
+
"""Response schema for a feature."""
|
|
117
|
+
id: int
|
|
118
|
+
priority: int
|
|
119
|
+
passes: bool
|
|
120
|
+
in_progress: bool
|
|
121
|
+
blocked: bool = False # Computed: has unmet dependencies
|
|
122
|
+
blocking_dependencies: list[int] = Field(default_factory=list) # Computed
|
|
123
|
+
|
|
124
|
+
class Config:
|
|
125
|
+
from_attributes = True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class FeatureListResponse(BaseModel):
|
|
129
|
+
"""Response containing list of features organized by status."""
|
|
130
|
+
pending: list[FeatureResponse]
|
|
131
|
+
in_progress: list[FeatureResponse]
|
|
132
|
+
done: list[FeatureResponse]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class FeatureBulkCreate(BaseModel):
|
|
136
|
+
"""Request schema for bulk creating features."""
|
|
137
|
+
features: list[FeatureCreate]
|
|
138
|
+
starting_priority: int | None = None # If None, appends after max priority
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FeatureBulkCreateResponse(BaseModel):
|
|
142
|
+
"""Response for bulk feature creation."""
|
|
143
|
+
created: int
|
|
144
|
+
features: list[FeatureResponse]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# Dependency Graph Schemas
|
|
149
|
+
# ============================================================================
|
|
150
|
+
|
|
151
|
+
class DependencyGraphNode(BaseModel):
|
|
152
|
+
"""Minimal node for graph visualization (no description exposed for security)."""
|
|
153
|
+
id: int
|
|
154
|
+
name: str
|
|
155
|
+
category: str
|
|
156
|
+
status: Literal["pending", "in_progress", "done", "blocked"]
|
|
157
|
+
priority: int
|
|
158
|
+
dependencies: list[int]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DependencyGraphEdge(BaseModel):
|
|
162
|
+
"""Edge in the dependency graph."""
|
|
163
|
+
source: int
|
|
164
|
+
target: int
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DependencyGraphResponse(BaseModel):
|
|
168
|
+
"""Response for dependency graph visualization."""
|
|
169
|
+
nodes: list[DependencyGraphNode]
|
|
170
|
+
edges: list[DependencyGraphEdge]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class DependencyUpdate(BaseModel):
|
|
174
|
+
"""Request schema for updating a feature's dependencies."""
|
|
175
|
+
dependency_ids: list[int] = Field(..., max_length=20) # Security: limit
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ============================================================================
|
|
179
|
+
# Agent Schemas
|
|
180
|
+
# ============================================================================
|
|
181
|
+
|
|
182
|
+
class AgentStartRequest(BaseModel):
|
|
183
|
+
"""Request schema for starting the agent."""
|
|
184
|
+
yolo_mode: bool | None = None # None means use global settings
|
|
185
|
+
model: str | None = None # None means use global settings
|
|
186
|
+
parallel_mode: bool | None = None # DEPRECATED: Use max_concurrency instead
|
|
187
|
+
max_concurrency: int | None = None # Max concurrent coding agents (1-5)
|
|
188
|
+
testing_agent_ratio: int | None = None # Regression testing agents (0-3)
|
|
189
|
+
|
|
190
|
+
@field_validator('model')
|
|
191
|
+
@classmethod
|
|
192
|
+
def validate_model(cls, v: str | None) -> str | None:
|
|
193
|
+
"""Validate model is in the allowed list."""
|
|
194
|
+
if v is not None and v not in VALID_MODELS:
|
|
195
|
+
raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
|
|
196
|
+
return v
|
|
197
|
+
|
|
198
|
+
@field_validator('max_concurrency')
|
|
199
|
+
@classmethod
|
|
200
|
+
def validate_concurrency(cls, v: int | None) -> int | None:
|
|
201
|
+
"""Validate max_concurrency is between 1 and 5."""
|
|
202
|
+
if v is not None and (v < 1 or v > 5):
|
|
203
|
+
raise ValueError("max_concurrency must be between 1 and 5")
|
|
204
|
+
return v
|
|
205
|
+
|
|
206
|
+
@field_validator('testing_agent_ratio')
|
|
207
|
+
@classmethod
|
|
208
|
+
def validate_testing_ratio(cls, v: int | None) -> int | None:
|
|
209
|
+
"""Validate testing_agent_ratio is between 0 and 3."""
|
|
210
|
+
if v is not None and (v < 0 or v > 3):
|
|
211
|
+
raise ValueError("testing_agent_ratio must be between 0 and 3")
|
|
212
|
+
return v
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class AgentStatus(BaseModel):
|
|
216
|
+
"""Current agent status."""
|
|
217
|
+
status: Literal["stopped", "running", "paused", "crashed"]
|
|
218
|
+
pid: int | None = None
|
|
219
|
+
started_at: datetime | None = None
|
|
220
|
+
yolo_mode: bool = False
|
|
221
|
+
model: str | None = None # Model being used by running agent
|
|
222
|
+
parallel_mode: bool = False # DEPRECATED: Always True now (unified orchestrator)
|
|
223
|
+
max_concurrency: int | None = None
|
|
224
|
+
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class AgentActionResponse(BaseModel):
|
|
228
|
+
"""Response for agent control actions."""
|
|
229
|
+
success: bool
|
|
230
|
+
status: str
|
|
231
|
+
message: str = ""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ============================================================================
|
|
235
|
+
# Setup Schemas
|
|
236
|
+
# ============================================================================
|
|
237
|
+
|
|
238
|
+
class SetupStatus(BaseModel):
|
|
239
|
+
"""System setup status."""
|
|
240
|
+
claude_cli: bool
|
|
241
|
+
credentials: bool
|
|
242
|
+
node: bool
|
|
243
|
+
npm: bool
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ============================================================================
|
|
247
|
+
# WebSocket Message Schemas
|
|
248
|
+
# ============================================================================
|
|
249
|
+
|
|
250
|
+
class WSProgressMessage(BaseModel):
|
|
251
|
+
"""WebSocket message for progress updates."""
|
|
252
|
+
type: Literal["progress"] = "progress"
|
|
253
|
+
passing: int
|
|
254
|
+
in_progress: int
|
|
255
|
+
total: int
|
|
256
|
+
percentage: float
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class WSFeatureUpdateMessage(BaseModel):
|
|
260
|
+
"""WebSocket message for feature status updates."""
|
|
261
|
+
type: Literal["feature_update"] = "feature_update"
|
|
262
|
+
feature_id: int
|
|
263
|
+
passes: bool
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class WSLogMessage(BaseModel):
|
|
267
|
+
"""WebSocket message for agent log output."""
|
|
268
|
+
type: Literal["log"] = "log"
|
|
269
|
+
line: str
|
|
270
|
+
timestamp: datetime
|
|
271
|
+
featureId: int | None = None
|
|
272
|
+
agentIndex: int | None = None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class WSAgentStatusMessage(BaseModel):
|
|
276
|
+
"""WebSocket message for agent status changes."""
|
|
277
|
+
type: Literal["agent_status"] = "agent_status"
|
|
278
|
+
status: str
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Agent state for multi-agent tracking
|
|
282
|
+
AgentState = Literal["idle", "thinking", "working", "testing", "success", "error", "struggling"]
|
|
283
|
+
|
|
284
|
+
# Agent type (coding vs testing)
|
|
285
|
+
AgentType = Literal["coding", "testing"]
|
|
286
|
+
|
|
287
|
+
# Agent mascot names assigned by index
|
|
288
|
+
AGENT_MASCOTS = ["Spark", "Fizz", "Octo", "Hoot", "Buzz"]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class WSAgentUpdateMessage(BaseModel):
|
|
292
|
+
"""WebSocket message for multi-agent status updates."""
|
|
293
|
+
type: Literal["agent_update"] = "agent_update"
|
|
294
|
+
agentIndex: int
|
|
295
|
+
agentName: str # One of AGENT_MASCOTS
|
|
296
|
+
agentType: AgentType = "coding" # "coding" or "testing"
|
|
297
|
+
featureId: int
|
|
298
|
+
featureName: str
|
|
299
|
+
state: AgentState
|
|
300
|
+
thought: str | None = None
|
|
301
|
+
timestamp: datetime
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ============================================================================
|
|
305
|
+
# Spec Chat Schemas
|
|
306
|
+
# ============================================================================
|
|
307
|
+
|
|
308
|
+
# Maximum image file size: 5 MB
|
|
309
|
+
MAX_IMAGE_SIZE = 5 * 1024 * 1024
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class ImageAttachment(BaseModel):
|
|
313
|
+
"""Image attachment from client for spec creation chat."""
|
|
314
|
+
filename: str = Field(..., min_length=1, max_length=255)
|
|
315
|
+
mimeType: Literal['image/jpeg', 'image/png']
|
|
316
|
+
base64Data: str
|
|
317
|
+
|
|
318
|
+
@field_validator('base64Data')
|
|
319
|
+
@classmethod
|
|
320
|
+
def validate_base64_and_size(cls, v: str) -> str:
|
|
321
|
+
"""Validate that base64 data is valid and within size limit."""
|
|
322
|
+
try:
|
|
323
|
+
decoded = base64.b64decode(v)
|
|
324
|
+
if len(decoded) > MAX_IMAGE_SIZE:
|
|
325
|
+
raise ValueError(
|
|
326
|
+
f'Image size ({len(decoded) / (1024 * 1024):.1f} MB) exceeds '
|
|
327
|
+
f'maximum of {MAX_IMAGE_SIZE // (1024 * 1024)} MB'
|
|
328
|
+
)
|
|
329
|
+
return v
|
|
330
|
+
except Exception as e:
|
|
331
|
+
if 'Image size' in str(e):
|
|
332
|
+
raise
|
|
333
|
+
raise ValueError(f'Invalid base64 data: {e}')
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ============================================================================
|
|
337
|
+
# Filesystem Schemas
|
|
338
|
+
# ============================================================================
|
|
339
|
+
|
|
340
|
+
class DriveInfo(BaseModel):
|
|
341
|
+
"""Information about a drive (Windows only)."""
|
|
342
|
+
letter: str
|
|
343
|
+
label: str
|
|
344
|
+
available: bool = True
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class DirectoryEntry(BaseModel):
|
|
348
|
+
"""An entry in a directory listing."""
|
|
349
|
+
name: str
|
|
350
|
+
path: str # POSIX format
|
|
351
|
+
is_directory: bool
|
|
352
|
+
is_hidden: bool = False
|
|
353
|
+
size: int | None = None # Bytes, for files
|
|
354
|
+
has_children: bool = False # True if directory has subdirectories
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class DirectoryListResponse(BaseModel):
|
|
358
|
+
"""Response for directory listing."""
|
|
359
|
+
current_path: str # POSIX format
|
|
360
|
+
parent_path: str | None
|
|
361
|
+
entries: list[DirectoryEntry]
|
|
362
|
+
drives: list[DriveInfo] | None = None # Windows only
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class PathValidationResponse(BaseModel):
|
|
366
|
+
"""Response for path validation."""
|
|
367
|
+
valid: bool
|
|
368
|
+
exists: bool
|
|
369
|
+
is_directory: bool
|
|
370
|
+
can_read: bool
|
|
371
|
+
can_write: bool
|
|
372
|
+
message: str = ""
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class CreateDirectoryRequest(BaseModel):
|
|
376
|
+
"""Request to create a new directory."""
|
|
377
|
+
parent_path: str
|
|
378
|
+
name: str = Field(..., min_length=1, max_length=255)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ============================================================================
|
|
382
|
+
# Settings Schemas
|
|
383
|
+
# ============================================================================
|
|
384
|
+
|
|
385
|
+
# Note: VALID_MODELS and DEFAULT_MODEL are imported from registry at the top of this file
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class ModelInfo(BaseModel):
|
|
389
|
+
"""Information about an available model."""
|
|
390
|
+
id: str
|
|
391
|
+
name: str
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class SettingsResponse(BaseModel):
|
|
395
|
+
"""Response schema for global settings."""
|
|
396
|
+
yolo_mode: bool = False
|
|
397
|
+
model: str = DEFAULT_MODEL
|
|
398
|
+
glm_mode: bool = False # True if GLM API is configured via .env
|
|
399
|
+
ollama_mode: bool = False # True if Ollama API is configured via .env
|
|
400
|
+
testing_agent_ratio: int = 1 # Regression testing agents (0-3)
|
|
401
|
+
playwright_headless: bool = True
|
|
402
|
+
batch_size: int = 3 # Features per coding agent batch (1-3)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class ModelsResponse(BaseModel):
|
|
406
|
+
"""Response schema for available models list."""
|
|
407
|
+
models: list[ModelInfo]
|
|
408
|
+
default: str
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class SettingsUpdate(BaseModel):
|
|
412
|
+
"""Request schema for updating global settings."""
|
|
413
|
+
yolo_mode: bool | None = None
|
|
414
|
+
model: str | None = None
|
|
415
|
+
testing_agent_ratio: int | None = None # 0-3
|
|
416
|
+
playwright_headless: bool | None = None
|
|
417
|
+
batch_size: int | None = None # Features per agent batch (1-3)
|
|
418
|
+
|
|
419
|
+
@field_validator('model')
|
|
420
|
+
@classmethod
|
|
421
|
+
def validate_model(cls, v: str | None) -> str | None:
|
|
422
|
+
if v is not None and v not in VALID_MODELS:
|
|
423
|
+
raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
|
|
424
|
+
return v
|
|
425
|
+
|
|
426
|
+
@field_validator('testing_agent_ratio')
|
|
427
|
+
@classmethod
|
|
428
|
+
def validate_testing_ratio(cls, v: int | None) -> int | None:
|
|
429
|
+
if v is not None and (v < 0 or v > 3):
|
|
430
|
+
raise ValueError("testing_agent_ratio must be between 0 and 3")
|
|
431
|
+
return v
|
|
432
|
+
|
|
433
|
+
@field_validator('batch_size')
|
|
434
|
+
@classmethod
|
|
435
|
+
def validate_batch_size(cls, v: int | None) -> int | None:
|
|
436
|
+
if v is not None and (v < 1 or v > 3):
|
|
437
|
+
raise ValueError("batch_size must be between 1 and 3")
|
|
438
|
+
return v
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ============================================================================
|
|
442
|
+
# Dev Server Schemas
|
|
443
|
+
# ============================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class DevServerStartRequest(BaseModel):
|
|
447
|
+
"""Request schema for starting the dev server."""
|
|
448
|
+
command: str | None = None # If None, uses effective command from config
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class DevServerStatus(BaseModel):
|
|
452
|
+
"""Current dev server status."""
|
|
453
|
+
status: Literal["stopped", "running", "crashed"]
|
|
454
|
+
pid: int | None = None
|
|
455
|
+
url: str | None = None
|
|
456
|
+
command: str | None = None
|
|
457
|
+
started_at: datetime | None = None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class DevServerActionResponse(BaseModel):
|
|
461
|
+
"""Response for dev server control actions."""
|
|
462
|
+
success: bool
|
|
463
|
+
status: Literal["stopped", "running", "crashed"]
|
|
464
|
+
message: str = ""
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class DevServerConfigResponse(BaseModel):
|
|
468
|
+
"""Response for dev server configuration."""
|
|
469
|
+
detected_type: str | None = None
|
|
470
|
+
detected_command: str | None = None
|
|
471
|
+
custom_command: str | None = None
|
|
472
|
+
effective_command: str | None = None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class DevServerConfigUpdate(BaseModel):
|
|
476
|
+
"""Request schema for updating dev server configuration."""
|
|
477
|
+
custom_command: str | None = None # None clears the custom command
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ============================================================================
|
|
481
|
+
# Dev Server WebSocket Message Schemas
|
|
482
|
+
# ============================================================================
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class WSDevLogMessage(BaseModel):
|
|
486
|
+
"""WebSocket message for dev server log output."""
|
|
487
|
+
type: Literal["dev_log"] = "dev_log"
|
|
488
|
+
line: str
|
|
489
|
+
timestamp: datetime
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class WSDevServerStatusMessage(BaseModel):
|
|
493
|
+
"""WebSocket message for dev server status changes."""
|
|
494
|
+
type: Literal["dev_server_status"] = "dev_server_status"
|
|
495
|
+
status: Literal["stopped", "running", "crashed"]
|
|
496
|
+
url: str | None = None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# ============================================================================
|
|
500
|
+
# Schedule Schemas
|
|
501
|
+
# ============================================================================
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class ScheduleCreate(BaseModel):
|
|
505
|
+
"""Request schema for creating a schedule."""
|
|
506
|
+
start_time: str = Field(
|
|
507
|
+
...,
|
|
508
|
+
pattern=r'^([0-1][0-9]|2[0-3]):[0-5][0-9]$',
|
|
509
|
+
description="Start time in HH:MM format (local time, will be stored as UTC)"
|
|
510
|
+
)
|
|
511
|
+
duration_minutes: int = Field(
|
|
512
|
+
...,
|
|
513
|
+
ge=1,
|
|
514
|
+
le=1440,
|
|
515
|
+
description="Duration in minutes (1-1440)"
|
|
516
|
+
)
|
|
517
|
+
days_of_week: int = Field(
|
|
518
|
+
default=127,
|
|
519
|
+
ge=0,
|
|
520
|
+
le=127,
|
|
521
|
+
description="Bitfield: Mon=1, Tue=2, Wed=4, Thu=8, Fri=16, Sat=32, Sun=64"
|
|
522
|
+
)
|
|
523
|
+
enabled: bool = True
|
|
524
|
+
yolo_mode: bool = False
|
|
525
|
+
model: str | None = None
|
|
526
|
+
max_concurrency: int = Field(
|
|
527
|
+
default=3,
|
|
528
|
+
ge=1,
|
|
529
|
+
le=5,
|
|
530
|
+
description="Max concurrent agents (1-5)"
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
@field_validator('model')
|
|
534
|
+
@classmethod
|
|
535
|
+
def validate_model(cls, v: str | None) -> str | None:
|
|
536
|
+
"""Validate model is in the allowed list."""
|
|
537
|
+
if v is not None and v not in VALID_MODELS:
|
|
538
|
+
raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
|
|
539
|
+
return v
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
class ScheduleUpdate(BaseModel):
|
|
543
|
+
"""Request schema for updating a schedule (partial updates allowed)."""
|
|
544
|
+
start_time: str | None = Field(
|
|
545
|
+
None,
|
|
546
|
+
pattern=r'^([0-1][0-9]|2[0-3]):[0-5][0-9]$'
|
|
547
|
+
)
|
|
548
|
+
duration_minutes: int | None = Field(None, ge=1, le=1440)
|
|
549
|
+
days_of_week: int | None = Field(None, ge=0, le=127)
|
|
550
|
+
enabled: bool | None = None
|
|
551
|
+
yolo_mode: bool | None = None
|
|
552
|
+
model: str | None = None
|
|
553
|
+
max_concurrency: int | None = Field(None, ge=1, le=5)
|
|
554
|
+
|
|
555
|
+
@field_validator('model')
|
|
556
|
+
@classmethod
|
|
557
|
+
def validate_model(cls, v: str | None) -> str | None:
|
|
558
|
+
"""Validate model is in the allowed list."""
|
|
559
|
+
if v is not None and v not in VALID_MODELS:
|
|
560
|
+
raise ValueError(f"Invalid model. Must be one of: {VALID_MODELS}")
|
|
561
|
+
return v
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class ScheduleResponse(BaseModel):
|
|
565
|
+
"""Response schema for a schedule."""
|
|
566
|
+
id: int
|
|
567
|
+
project_name: str
|
|
568
|
+
start_time: str # UTC, frontend converts to local
|
|
569
|
+
duration_minutes: int
|
|
570
|
+
days_of_week: int
|
|
571
|
+
enabled: bool
|
|
572
|
+
yolo_mode: bool
|
|
573
|
+
model: str | None
|
|
574
|
+
max_concurrency: int
|
|
575
|
+
crash_count: int
|
|
576
|
+
created_at: datetime
|
|
577
|
+
|
|
578
|
+
class Config:
|
|
579
|
+
from_attributes = True
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class ScheduleListResponse(BaseModel):
|
|
583
|
+
"""Response containing list of schedules."""
|
|
584
|
+
schedules: list[ScheduleResponse]
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class NextRunResponse(BaseModel):
|
|
588
|
+
"""Response for next scheduled run calculation."""
|
|
589
|
+
has_schedules: bool
|
|
590
|
+
next_start: datetime | None # UTC
|
|
591
|
+
next_end: datetime | None # UTC (latest end if overlapping)
|
|
592
|
+
is_currently_running: bool
|
|
593
|
+
active_schedule_count: int
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend Services
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
Business logic and process management services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .process_manager import AgentProcessManager
|
|
9
|
+
from .project_config import (
|
|
10
|
+
clear_dev_command,
|
|
11
|
+
detect_project_type,
|
|
12
|
+
get_default_dev_command,
|
|
13
|
+
get_dev_command,
|
|
14
|
+
get_project_config,
|
|
15
|
+
set_dev_command,
|
|
16
|
+
)
|
|
17
|
+
from .terminal_manager import (
|
|
18
|
+
TerminalSession,
|
|
19
|
+
cleanup_all_terminals,
|
|
20
|
+
get_terminal_session,
|
|
21
|
+
remove_terminal_session,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AgentProcessManager",
|
|
26
|
+
"TerminalSession",
|
|
27
|
+
"cleanup_all_terminals",
|
|
28
|
+
"clear_dev_command",
|
|
29
|
+
"detect_project_type",
|
|
30
|
+
"get_default_dev_command",
|
|
31
|
+
"get_dev_command",
|
|
32
|
+
"get_project_config",
|
|
33
|
+
"get_terminal_session",
|
|
34
|
+
"remove_terminal_session",
|
|
35
|
+
"set_dev_command",
|
|
36
|
+
]
|