codeframe-ai 0.9.0__py3-none-any.whl

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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,201 @@
1
+ """Standardized API response models for CodeFRAME v2.
2
+
3
+ This module provides consistent response and error formats for all v2 API endpoints.
4
+
5
+ Standard Response Format:
6
+ {
7
+ "success": true,
8
+ "data": { ... },
9
+ "message": "Optional human-readable message"
10
+ }
11
+
12
+ Standard Error Format:
13
+ {
14
+ "error": "Error description",
15
+ "detail": "Additional context",
16
+ "code": "ERROR_CODE"
17
+ }
18
+
19
+ Usage:
20
+ from codeframe.ui.response_models import ApiResponse, api_response, ApiError
21
+
22
+ @router.get("/items")
23
+ async def list_items() -> ApiResponse[list[Item]]:
24
+ items = get_items()
25
+ return api_response(items, message="Retrieved 5 items")
26
+
27
+ # For errors, raise HTTPException with ApiError detail
28
+ raise HTTPException(status_code=404, detail=ApiError(
29
+ error="Item not found",
30
+ detail=f"No item with id {item_id}",
31
+ code="ITEM_NOT_FOUND"
32
+ ).model_dump())
33
+ """
34
+
35
+ from typing import Any, Generic, Optional, TypeVar
36
+
37
+ from pydantic import BaseModel, Field
38
+
39
+ T = TypeVar("T")
40
+
41
+
42
+ # ============================================================================
43
+ # Standard Response Models
44
+ # ============================================================================
45
+
46
+
47
+ class ApiResponse(BaseModel, Generic[T]):
48
+ """Standard API response wrapper.
49
+
50
+ All successful responses should use this format for consistency.
51
+ """
52
+
53
+ success: bool = Field(default=True, description="Whether the request succeeded")
54
+ data: T = Field(..., description="Response payload")
55
+ message: Optional[str] = Field(default=None, description="Optional human-readable message")
56
+
57
+
58
+ class ApiError(BaseModel):
59
+ """Standard API error format.
60
+
61
+ Use this for HTTPException details to ensure consistent error responses.
62
+ """
63
+
64
+ error: str = Field(..., description="Error description")
65
+ detail: Optional[str] = Field(default=None, description="Additional context")
66
+ code: str = Field(..., description="Machine-readable error code")
67
+
68
+
69
+ class PaginatedResponse(BaseModel, Generic[T]):
70
+ """Standard paginated response wrapper."""
71
+
72
+ success: bool = Field(default=True)
73
+ data: list[T] = Field(..., description="List of items")
74
+ total: int = Field(..., description="Total number of items")
75
+ page: int = Field(..., description="Current page number")
76
+ page_size: int = Field(..., description="Items per page")
77
+ message: Optional[str] = Field(default=None)
78
+
79
+
80
+ # ============================================================================
81
+ # Helper Functions
82
+ # ============================================================================
83
+
84
+
85
+ def api_response(
86
+ data: Any,
87
+ message: Optional[str] = None,
88
+ ) -> dict:
89
+ """Create a standard API response dict.
90
+
91
+ Args:
92
+ data: Response payload
93
+ message: Optional human-readable message
94
+
95
+ Returns:
96
+ Dict in standard response format
97
+
98
+ Example:
99
+ return api_response({"id": 1, "name": "test"}, message="Created successfully")
100
+ """
101
+ response = {
102
+ "success": True,
103
+ "data": data,
104
+ }
105
+ if message:
106
+ response["message"] = message
107
+ return response
108
+
109
+
110
+ def api_error(
111
+ error: str,
112
+ code: str,
113
+ detail: Optional[str] = None,
114
+ ) -> dict:
115
+ """Create a standard API error dict for HTTPException detail.
116
+
117
+ Args:
118
+ error: Error description
119
+ code: Machine-readable error code
120
+ detail: Additional context
121
+
122
+ Returns:
123
+ Dict in standard error format
124
+
125
+ Example:
126
+ raise HTTPException(
127
+ status_code=404,
128
+ detail=api_error("Not found", "ITEM_NOT_FOUND", f"No item {id}")
129
+ )
130
+ """
131
+ result = {
132
+ "error": error,
133
+ "code": code,
134
+ }
135
+ if detail:
136
+ result["detail"] = detail
137
+ return result
138
+
139
+
140
+ # ============================================================================
141
+ # Common Error Codes
142
+ # ============================================================================
143
+
144
+
145
+ class ErrorCodes:
146
+ """Standard error codes for consistent error handling."""
147
+
148
+ # Resource errors (4xx)
149
+ NOT_FOUND = "NOT_FOUND"
150
+ ALREADY_EXISTS = "ALREADY_EXISTS"
151
+ INVALID_REQUEST = "INVALID_REQUEST"
152
+ VALIDATION_ERROR = "VALIDATION_ERROR"
153
+
154
+ # Authorization errors
155
+ UNAUTHORIZED = "UNAUTHORIZED"
156
+ FORBIDDEN = "FORBIDDEN"
157
+
158
+ # State errors
159
+ INVALID_STATE = "INVALID_STATE"
160
+ CONFLICT = "CONFLICT"
161
+
162
+ # Execution errors
163
+ EXECUTION_FAILED = "EXECUTION_FAILED"
164
+ TIMEOUT = "TIMEOUT"
165
+
166
+ # Server errors (5xx)
167
+ INTERNAL_ERROR = "INTERNAL_ERROR"
168
+ SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
169
+
170
+
171
+ # ============================================================================
172
+ # Common Success Messages
173
+ # ============================================================================
174
+
175
+
176
+ def created_message(resource: str, id: Any = None) -> str:
177
+ """Generate a standard created message."""
178
+ if id:
179
+ return f"{resource} created successfully (id: {id})"
180
+ return f"{resource} created successfully"
181
+
182
+
183
+ def updated_message(resource: str, id: Any = None) -> str:
184
+ """Generate a standard updated message."""
185
+ if id:
186
+ return f"{resource} updated successfully (id: {id})"
187
+ return f"{resource} updated successfully"
188
+
189
+
190
+ def deleted_message(resource: str, id: Any = None) -> str:
191
+ """Generate a standard deleted message."""
192
+ if id:
193
+ return f"{resource} deleted successfully (id: {id})"
194
+ return f"{resource} deleted successfully"
195
+
196
+
197
+ def retrieved_message(resource: str, count: Optional[int] = None) -> str:
198
+ """Generate a standard retrieval message."""
199
+ if count is not None:
200
+ return f"Retrieved {count} {resource}(s)"
201
+ return f"{resource} retrieved successfully"
@@ -0,0 +1,5 @@
1
+ """FastAPI router modules.
2
+
3
+ This package contains all API router modules, organized by domain.
4
+ Each router handles a specific set of related endpoints.
5
+ """
@@ -0,0 +1,29 @@
1
+ """Shared helpers for v2 routers."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+
9
+ def atomic_write_json(path: Path, payload: dict) -> None:
10
+ """Write JSON via per-call unique temp-file + os.replace.
11
+
12
+ A unique temp name is required so concurrent writers to the same target
13
+ do not collide on a shared `.tmp` suffix.
14
+ """
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ fd, tmp_name = tempfile.mkstemp(
17
+ prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
18
+ )
19
+ try:
20
+ with os.fdopen(fd, "w") as f:
21
+ f.write(json.dumps(payload, indent=2))
22
+ os.replace(tmp_name, path)
23
+ except Exception:
24
+ # Clean up the temp file on failure so we don't leak it.
25
+ try:
26
+ os.unlink(tmp_name)
27
+ except OSError:
28
+ pass
29
+ raise
@@ -0,0 +1,315 @@
1
+ """V2 Batches router - delegates to core/conductor module.
2
+
3
+ This module provides v2-style API endpoints for batch execution management.
4
+ Batches represent coordinated execution of multiple tasks.
5
+
6
+ Routes:
7
+ GET /api/v2/batches - List batches
8
+ GET /api/v2/batches/{id} - Get batch status
9
+ POST /api/v2/batches/{id}/stop - Stop a running batch
10
+ POST /api/v2/batches/{id}/resume - Resume a batch
11
+ """
12
+
13
+ import logging
14
+ from typing import Optional
15
+
16
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
17
+ from pydantic import BaseModel, Field
18
+
19
+ from codeframe.core.workspace import Workspace
20
+ from codeframe.lib.rate_limiter import rate_limit_standard
21
+ from codeframe.core import conductor
22
+ from codeframe.core.conductor import BatchStatus
23
+ from codeframe.ui.dependencies import get_v2_workspace
24
+ from codeframe.ui.response_models import api_error, ErrorCodes
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ router = APIRouter(prefix="/api/v2/batches", tags=["batches-v2"])
29
+
30
+
31
+ # ============================================================================
32
+ # Request/Response Models
33
+ # ============================================================================
34
+
35
+
36
+ class BatchResponse(BaseModel):
37
+ """Response for a single batch."""
38
+
39
+ id: str
40
+ workspace_id: str
41
+ task_ids: list[str]
42
+ status: str
43
+ strategy: str
44
+ max_parallel: int
45
+ on_failure: str
46
+ started_at: Optional[str]
47
+ completed_at: Optional[str]
48
+ results: dict[str, str] # task_id -> RunStatus value
49
+
50
+
51
+ class BatchListResponse(BaseModel):
52
+ """Response for batch list."""
53
+
54
+ batches: list[BatchResponse]
55
+ total: int
56
+ by_status: dict[str, int]
57
+
58
+
59
+ class StopBatchRequest(BaseModel):
60
+ """Request for stopping a batch."""
61
+
62
+ force: bool = Field(False, description="Force stop by terminating running processes")
63
+
64
+
65
+ class ResumeBatchRequest(BaseModel):
66
+ """Request for resuming a batch."""
67
+
68
+ force: bool = Field(False, description="Re-run all tasks including completed ones")
69
+
70
+
71
+ # ============================================================================
72
+ # Helper Functions
73
+ # ============================================================================
74
+
75
+
76
+ def _batch_to_response(batch: conductor.BatchRun) -> BatchResponse:
77
+ """Convert a BatchRun to a BatchResponse."""
78
+ return BatchResponse(
79
+ id=batch.id,
80
+ workspace_id=batch.workspace_id,
81
+ task_ids=batch.task_ids,
82
+ status=batch.status.value,
83
+ strategy=batch.strategy,
84
+ max_parallel=batch.max_parallel,
85
+ on_failure=batch.on_failure.value,
86
+ started_at=batch.started_at.isoformat() if batch.started_at else None,
87
+ completed_at=batch.completed_at.isoformat() if batch.completed_at else None,
88
+ results={k: v.value if hasattr(v, 'value') else str(v) for k, v in batch.results.items()},
89
+ )
90
+
91
+
92
+ # ============================================================================
93
+ # Endpoints
94
+ # ============================================================================
95
+
96
+
97
+ @router.get("", response_model=BatchListResponse)
98
+ @rate_limit_standard()
99
+ async def list_batches(
100
+ request: Request,
101
+ status: Optional[str] = Query(None, description="Filter by status (PENDING, RUNNING, COMPLETED, PARTIAL, FAILED, CANCELLED)"),
102
+ limit: int = Query(20, ge=1, le=100),
103
+ workspace: Workspace = Depends(get_v2_workspace),
104
+ ) -> BatchListResponse:
105
+ """List batches in the workspace.
106
+
107
+ Args:
108
+ status: Optional status filter
109
+ limit: Maximum batches to return
110
+ workspace: v2 Workspace
111
+
112
+ Returns:
113
+ List of batches with counts by status
114
+ """
115
+ # Parse status filter
116
+ status_filter = None
117
+ if status:
118
+ try:
119
+ status_filter = BatchStatus(status.upper())
120
+ except ValueError:
121
+ raise HTTPException(
122
+ status_code=400,
123
+ detail=api_error(
124
+ f"Invalid status: {status}",
125
+ ErrorCodes.VALIDATION_ERROR,
126
+ f"Valid values: {[s.value for s in BatchStatus]}",
127
+ ),
128
+ )
129
+
130
+ # Get batches
131
+ batch_list = conductor.list_batches(workspace, status=status_filter, limit=limit)
132
+
133
+ # Calculate counts by status
134
+ all_batches = conductor.list_batches(workspace, limit=1000)
135
+ status_counts: dict[str, int] = {}
136
+ for batch in all_batches:
137
+ status_val = batch.status.value
138
+ status_counts[status_val] = status_counts.get(status_val, 0) + 1
139
+
140
+ return BatchListResponse(
141
+ batches=[_batch_to_response(b) for b in batch_list],
142
+ total=len(batch_list),
143
+ by_status=status_counts,
144
+ )
145
+
146
+
147
+ @router.get("/{batch_id}", response_model=BatchResponse)
148
+ @rate_limit_standard()
149
+ async def get_batch(
150
+ request: Request,
151
+ batch_id: str,
152
+ workspace: Workspace = Depends(get_v2_workspace),
153
+ ) -> BatchResponse:
154
+ """Get a specific batch by ID.
155
+
156
+ Args:
157
+ batch_id: Batch identifier
158
+ workspace: v2 Workspace
159
+
160
+ Returns:
161
+ Batch details
162
+
163
+ Raises:
164
+ HTTPException: 404 if batch not found
165
+ """
166
+ batch = conductor.get_batch(workspace, batch_id)
167
+
168
+ if not batch:
169
+ raise HTTPException(
170
+ status_code=404,
171
+ detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, f"No batch with id {batch_id}"),
172
+ )
173
+
174
+ return _batch_to_response(batch)
175
+
176
+
177
+ @router.post("/{batch_id}/stop", response_model=BatchResponse)
178
+ @rate_limit_standard()
179
+ async def stop_batch(
180
+ request: Request,
181
+ batch_id: str,
182
+ body: StopBatchRequest = None,
183
+ workspace: Workspace = Depends(get_v2_workspace),
184
+ ) -> BatchResponse:
185
+ """Stop a running batch.
186
+
187
+ Graceful stop (force=False):
188
+ - Marks batch as CANCELLED
189
+ - Running tasks finish naturally
190
+
191
+ Force stop (force=True):
192
+ - Sends SIGTERM to running processes
193
+ - Immediate termination
194
+
195
+ Args:
196
+ request: HTTP request for rate limiting
197
+ batch_id: Batch to stop
198
+ body: Stop options
199
+ workspace: v2 Workspace
200
+
201
+ Returns:
202
+ Updated batch
203
+
204
+ Raises:
205
+ HTTPException:
206
+ - 404: Batch not found
207
+ - 400: Batch not in stoppable state
208
+ """
209
+ force = body.force if body else False
210
+
211
+ try:
212
+ batch = conductor.stop_batch(workspace, batch_id, force=force)
213
+ return _batch_to_response(batch)
214
+
215
+ except ValueError as e:
216
+ error_msg = str(e)
217
+ if "not found" in error_msg.lower():
218
+ raise HTTPException(
219
+ status_code=404,
220
+ detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
221
+ )
222
+ raise HTTPException(
223
+ status_code=400,
224
+ detail=api_error("Cannot stop batch", ErrorCodes.INVALID_STATE, error_msg),
225
+ )
226
+
227
+
228
+ @router.post("/{batch_id}/resume", response_model=BatchResponse)
229
+ @rate_limit_standard()
230
+ async def resume_batch(
231
+ request: Request,
232
+ batch_id: str,
233
+ body: ResumeBatchRequest = None,
234
+ workspace: Workspace = Depends(get_v2_workspace),
235
+ ) -> BatchResponse:
236
+ """Resume a batch by re-running failed/blocked tasks.
237
+
238
+ Args:
239
+ request: HTTP request for rate limiting
240
+ batch_id: Batch to resume
241
+ body: Resume options
242
+ workspace: v2 Workspace
243
+
244
+ Returns:
245
+ Updated batch
246
+
247
+ Raises:
248
+ HTTPException:
249
+ - 404: Batch not found
250
+ - 400: Batch not in resumable state
251
+ """
252
+ force = body.force if body else False
253
+
254
+ try:
255
+ batch = conductor.resume_batch(workspace, batch_id, force=force)
256
+ return _batch_to_response(batch)
257
+
258
+ except ValueError as e:
259
+ error_msg = str(e)
260
+ if "not found" in error_msg.lower():
261
+ raise HTTPException(
262
+ status_code=404,
263
+ detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
264
+ )
265
+ raise HTTPException(
266
+ status_code=400,
267
+ detail=api_error("Cannot resume batch", ErrorCodes.INVALID_STATE, error_msg),
268
+ )
269
+
270
+ except Exception as e:
271
+ logger.error(f"Failed to resume batch {batch_id}: {e}", exc_info=True)
272
+ raise HTTPException(
273
+ status_code=500,
274
+ detail=api_error("Resume failed", ErrorCodes.EXECUTION_FAILED, str(e)),
275
+ )
276
+
277
+
278
+ @router.post("/{batch_id}/cancel", response_model=BatchResponse)
279
+ @rate_limit_standard()
280
+ async def cancel_batch(
281
+ request: Request,
282
+ batch_id: str,
283
+ workspace: Workspace = Depends(get_v2_workspace),
284
+ ) -> BatchResponse:
285
+ """Cancel a running batch.
286
+
287
+ Similar to stop but with explicit cancel semantics.
288
+
289
+ Args:
290
+ batch_id: Batch to cancel
291
+ workspace: v2 Workspace
292
+
293
+ Returns:
294
+ Updated batch
295
+
296
+ Raises:
297
+ HTTPException:
298
+ - 404: Batch not found
299
+ - 400: Batch not in cancellable state
300
+ """
301
+ try:
302
+ batch = conductor.cancel_batch(workspace, batch_id)
303
+ return _batch_to_response(batch)
304
+
305
+ except ValueError as e:
306
+ error_msg = str(e)
307
+ if "not found" in error_msg.lower():
308
+ raise HTTPException(
309
+ status_code=404,
310
+ detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
311
+ )
312
+ raise HTTPException(
313
+ status_code=400,
314
+ detail=api_error("Cannot cancel batch", ErrorCodes.INVALID_STATE, error_msg),
315
+ )