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,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
+ ]