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,320 @@
1
+ """V2 Blockers router - delegates to core/blockers module.
2
+
3
+ This module provides v2-style API endpoints for blocker management (human-in-the-loop).
4
+ Blockers represent questions that pause task execution until answered by a human.
5
+
6
+ Routes:
7
+ GET /api/v2/blockers - List blockers with optional filters
8
+ GET /api/v2/blockers/{id} - Get a specific blocker
9
+ POST /api/v2/blockers - Create a new blocker
10
+ POST /api/v2/blockers/{id}/answer - Answer a blocker
11
+ POST /api/v2/blockers/{id}/resolve - Mark blocker as resolved
12
+ """
13
+
14
+ import logging
15
+ from typing import Optional
16
+
17
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
18
+ from pydantic import BaseModel, Field
19
+
20
+ from codeframe.core.workspace import Workspace
21
+ from codeframe.lib.rate_limiter import rate_limit_standard
22
+ from codeframe.core import blockers
23
+ from codeframe.core.blockers import BlockerStatus, BlockerOrigin
24
+ from codeframe.ui.dependencies import get_v2_workspace
25
+ from codeframe.ui.response_models import api_error, ErrorCodes
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ router = APIRouter(prefix="/api/v2/blockers", tags=["blockers-v2"])
30
+
31
+
32
+ # ============================================================================
33
+ # Request/Response Models
34
+ # ============================================================================
35
+
36
+
37
+ class BlockerResponse(BaseModel):
38
+ """Response for a single blocker."""
39
+
40
+ id: str
41
+ workspace_id: str
42
+ task_id: Optional[str]
43
+ question: str
44
+ answer: Optional[str]
45
+ status: str
46
+ created_at: str
47
+ answered_at: Optional[str]
48
+ created_by: BlockerOrigin = BlockerOrigin.HUMAN
49
+
50
+
51
+ class BlockerListResponse(BaseModel):
52
+ """Response for blocker list."""
53
+
54
+ blockers: list[BlockerResponse]
55
+ total: int
56
+ by_status: dict[str, int]
57
+
58
+
59
+ class CreateBlockerRequest(BaseModel):
60
+ """Request for creating a blocker."""
61
+
62
+ question: str = Field(..., min_length=1, description="The question to ask")
63
+ task_id: Optional[str] = Field(None, description="Optional associated task ID")
64
+
65
+
66
+ class AnswerBlockerRequest(BaseModel):
67
+ """Request for answering a blocker."""
68
+
69
+ answer: str = Field(..., min_length=1, description="The answer text")
70
+
71
+
72
+ # ============================================================================
73
+ # Helper Functions
74
+ # ============================================================================
75
+
76
+
77
+ def _blocker_to_response(blocker: blockers.Blocker) -> BlockerResponse:
78
+ """Convert a Blocker to a BlockerResponse."""
79
+ return BlockerResponse(
80
+ id=blocker.id,
81
+ workspace_id=blocker.workspace_id,
82
+ task_id=blocker.task_id,
83
+ question=blocker.question,
84
+ answer=blocker.answer,
85
+ status=blocker.status.value,
86
+ created_at=blocker.created_at.isoformat(),
87
+ answered_at=blocker.answered_at.isoformat() if blocker.answered_at else None,
88
+ created_by=blocker.created_by.value,
89
+ )
90
+
91
+
92
+ # ============================================================================
93
+ # Endpoints
94
+ # ============================================================================
95
+
96
+
97
+ @router.get("", response_model=BlockerListResponse)
98
+ @rate_limit_standard()
99
+ async def list_blockers(
100
+ request: Request,
101
+ status: Optional[str] = Query(None, description="Filter by status (OPEN, ANSWERED, RESOLVED)"),
102
+ task_id: Optional[str] = Query(None, description="Filter by task ID"),
103
+ limit: int = Query(100, ge=1, le=1000),
104
+ workspace: Workspace = Depends(get_v2_workspace),
105
+ ) -> BlockerListResponse:
106
+ """List blockers in the workspace.
107
+
108
+ Args:
109
+ status: Optional status filter
110
+ task_id: Optional task filter
111
+ limit: Maximum blockers to return
112
+ workspace: v2 Workspace
113
+
114
+ Returns:
115
+ List of blockers with counts by status
116
+ """
117
+ # Parse status filter
118
+ status_filter = None
119
+ if status:
120
+ try:
121
+ status_filter = BlockerStatus(status.upper())
122
+ except ValueError:
123
+ raise HTTPException(
124
+ status_code=400,
125
+ detail=api_error(
126
+ f"Invalid status: {status}",
127
+ ErrorCodes.VALIDATION_ERROR,
128
+ f"Valid values: {[s.value for s in BlockerStatus]}",
129
+ ),
130
+ )
131
+
132
+ # Get blockers
133
+ blocker_list = blockers.list_all(
134
+ workspace,
135
+ status=status_filter,
136
+ task_id=task_id,
137
+ limit=limit,
138
+ )
139
+
140
+ # Get counts by status
141
+ status_counts = blockers.count_by_status(workspace)
142
+
143
+ return BlockerListResponse(
144
+ blockers=[_blocker_to_response(b) for b in blocker_list],
145
+ total=len(blocker_list),
146
+ by_status=status_counts,
147
+ )
148
+
149
+
150
+ @router.get("/{blocker_id}", response_model=BlockerResponse)
151
+ @rate_limit_standard()
152
+ async def get_blocker(
153
+ request: Request,
154
+ blocker_id: str,
155
+ workspace: Workspace = Depends(get_v2_workspace),
156
+ ) -> BlockerResponse:
157
+ """Get a specific blocker by ID.
158
+
159
+ Supports partial ID matching (prefix).
160
+
161
+ Args:
162
+ blocker_id: Blocker identifier (can be partial)
163
+ workspace: v2 Workspace
164
+
165
+ Returns:
166
+ Blocker details
167
+
168
+ Raises:
169
+ HTTPException: 404 if blocker not found, 400 if multiple matches
170
+ """
171
+ try:
172
+ blocker = blockers.get(workspace, blocker_id)
173
+ except ValueError as e:
174
+ # Multiple blockers match partial ID
175
+ raise HTTPException(
176
+ status_code=400,
177
+ detail=api_error("Ambiguous blocker ID", ErrorCodes.VALIDATION_ERROR, str(e)),
178
+ )
179
+
180
+ if not blocker:
181
+ raise HTTPException(
182
+ status_code=404,
183
+ detail=api_error("Blocker not found", ErrorCodes.NOT_FOUND, f"No blocker with id {blocker_id}"),
184
+ )
185
+
186
+ return _blocker_to_response(blocker)
187
+
188
+
189
+ @router.post("", response_model=BlockerResponse, status_code=201)
190
+ @rate_limit_standard()
191
+ async def create_blocker(
192
+ request: Request,
193
+ body: CreateBlockerRequest,
194
+ workspace: Workspace = Depends(get_v2_workspace),
195
+ ) -> BlockerResponse:
196
+ """Create a new blocker.
197
+
198
+ Args:
199
+ request: HTTP request for rate limiting
200
+ body: Blocker creation request
201
+ workspace: v2 Workspace
202
+
203
+ Returns:
204
+ Created blocker
205
+ """
206
+ try:
207
+ blocker = blockers.create(
208
+ workspace,
209
+ question=body.question,
210
+ task_id=body.task_id,
211
+ )
212
+ return _blocker_to_response(blocker)
213
+
214
+ except Exception as e:
215
+ logger.error(f"Failed to create blocker: {e}", exc_info=True)
216
+ raise HTTPException(
217
+ status_code=500,
218
+ detail=api_error("Failed to create blocker", ErrorCodes.EXECUTION_FAILED, str(e)),
219
+ )
220
+
221
+
222
+ @router.post("/{blocker_id}/answer", response_model=BlockerResponse)
223
+ @rate_limit_standard()
224
+ async def answer_blocker(
225
+ request: Request,
226
+ blocker_id: str,
227
+ body: AnswerBlockerRequest,
228
+ workspace: Workspace = Depends(get_v2_workspace),
229
+ ) -> BlockerResponse:
230
+ """Answer a blocker.
231
+
232
+ Answering a blocker also resets the associated task to READY status,
233
+ so it can be restarted with `cf work start <task-id> --execute`.
234
+
235
+ Args:
236
+ request: HTTP request for rate limiting
237
+ blocker_id: Blocker to answer (can be partial ID)
238
+ body: Answer request
239
+ workspace: v2 Workspace
240
+
241
+ Returns:
242
+ Updated blocker
243
+
244
+ Raises:
245
+ HTTPException:
246
+ - 404: Blocker not found
247
+ - 400: Blocker already resolved or ambiguous ID
248
+ """
249
+ try:
250
+ blocker = blockers.answer(workspace, blocker_id, body.answer)
251
+ return _blocker_to_response(blocker)
252
+
253
+ except ValueError as e:
254
+ error_msg = str(e)
255
+ if "not found" in error_msg.lower():
256
+ raise HTTPException(
257
+ status_code=404,
258
+ detail=api_error("Blocker not found", ErrorCodes.NOT_FOUND, error_msg),
259
+ )
260
+ elif "already resolved" in error_msg.lower():
261
+ raise HTTPException(
262
+ status_code=400,
263
+ detail=api_error("Cannot answer", ErrorCodes.INVALID_STATE, error_msg),
264
+ )
265
+ else:
266
+ raise HTTPException(
267
+ status_code=400,
268
+ detail=api_error("Invalid request", ErrorCodes.VALIDATION_ERROR, error_msg),
269
+ )
270
+
271
+
272
+ @router.post("/{blocker_id}/resolve", response_model=BlockerResponse)
273
+ @rate_limit_standard()
274
+ async def resolve_blocker(
275
+ request: Request,
276
+ blocker_id: str,
277
+ workspace: Workspace = Depends(get_v2_workspace),
278
+ ) -> BlockerResponse:
279
+ """Mark a blocker as resolved.
280
+
281
+ A blocker must be answered before it can be resolved.
282
+
283
+ Args:
284
+ blocker_id: Blocker to resolve (can be partial ID)
285
+ workspace: v2 Workspace
286
+
287
+ Returns:
288
+ Updated blocker
289
+
290
+ Raises:
291
+ HTTPException:
292
+ - 404: Blocker not found
293
+ - 400: Blocker not answered yet or already resolved
294
+ """
295
+ try:
296
+ blocker = blockers.resolve(workspace, blocker_id)
297
+ return _blocker_to_response(blocker)
298
+
299
+ except ValueError as e:
300
+ error_msg = str(e)
301
+ if "not found" in error_msg.lower():
302
+ raise HTTPException(
303
+ status_code=404,
304
+ detail=api_error("Blocker not found", ErrorCodes.NOT_FOUND, error_msg),
305
+ )
306
+ elif "must be answered" in error_msg.lower():
307
+ raise HTTPException(
308
+ status_code=400,
309
+ detail=api_error("Cannot resolve", ErrorCodes.INVALID_STATE, error_msg),
310
+ )
311
+ elif "already resolved" in error_msg.lower():
312
+ raise HTTPException(
313
+ status_code=400,
314
+ detail=api_error("Cannot resolve", ErrorCodes.INVALID_STATE, error_msg),
315
+ )
316
+ else:
317
+ raise HTTPException(
318
+ status_code=400,
319
+ detail=api_error("Invalid request", ErrorCodes.VALIDATION_ERROR, error_msg),
320
+ )
@@ -0,0 +1,310 @@
1
+ """V2 Checkpoint router - delegates to core modules.
2
+
3
+ This module provides v2-style API endpoints for checkpoint management that
4
+ delegate to core/checkpoints.py. It uses the v2 Workspace model.
5
+
6
+ The v1 router (checkpoints.py) remains for backwards compatibility.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any, Optional
11
+
12
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
13
+ from pydantic import BaseModel, Field
14
+
15
+ from codeframe.core.workspace import Workspace
16
+ from codeframe.lib.rate_limiter import rate_limit_standard
17
+ from codeframe.core import checkpoints
18
+ from codeframe.ui.dependencies import get_v2_workspace
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ router = APIRouter(prefix="/api/v2/checkpoints", tags=["checkpoints-v2"])
23
+
24
+
25
+ # ============================================================================
26
+ # Request/Response Models
27
+ # ============================================================================
28
+
29
+
30
+ class CreateCheckpointRequest(BaseModel):
31
+ """Request for creating a checkpoint."""
32
+
33
+ name: str = Field(..., min_length=1, max_length=200)
34
+ include_git_ref: bool = Field(
35
+ default=True,
36
+ description="Whether to capture current git HEAD",
37
+ )
38
+
39
+
40
+ class CheckpointResponse(BaseModel):
41
+ """Response for a single checkpoint."""
42
+
43
+ id: str
44
+ name: str
45
+ created_at: str
46
+ summary: dict[str, Any]
47
+
48
+
49
+ class CheckpointListResponse(BaseModel):
50
+ """Response for checkpoint list."""
51
+
52
+ checkpoints: list[CheckpointResponse]
53
+ total: int
54
+
55
+
56
+ class TaskDiffResponse(BaseModel):
57
+ """Response for a single task diff."""
58
+
59
+ task_id: str
60
+ title: str
61
+ old_status: Optional[str]
62
+ new_status: Optional[str]
63
+ change_type: str
64
+
65
+
66
+ class CheckpointDiffResponse(BaseModel):
67
+ """Response for checkpoint diff."""
68
+
69
+ checkpoint_a: dict[str, str]
70
+ checkpoint_b: dict[str, str]
71
+ task_diffs: list[TaskDiffResponse]
72
+ summary: dict[str, int]
73
+
74
+
75
+ # ============================================================================
76
+ # Checkpoint Endpoints
77
+ # ============================================================================
78
+
79
+
80
+ @router.post("", response_model=CheckpointResponse)
81
+ @rate_limit_standard()
82
+ async def create_checkpoint(
83
+ request: Request,
84
+ body: CreateCheckpointRequest,
85
+ workspace: Workspace = Depends(get_v2_workspace),
86
+ ) -> CheckpointResponse:
87
+ """Create a new checkpoint.
88
+
89
+ Captures current state of tasks, blockers, and optionally git ref.
90
+
91
+ This is the v2 equivalent of `cf checkpoint create`.
92
+
93
+ Args:
94
+ request: HTTP request for rate limiting
95
+ body: Checkpoint creation request
96
+ workspace: v2 Workspace
97
+
98
+ Returns:
99
+ Created checkpoint
100
+ """
101
+ try:
102
+ checkpoint = checkpoints.create(
103
+ workspace,
104
+ name=body.name,
105
+ include_git_ref=body.include_git_ref,
106
+ )
107
+
108
+ return CheckpointResponse(
109
+ id=checkpoint.id,
110
+ name=checkpoint.name,
111
+ created_at=checkpoint.created_at.isoformat(),
112
+ summary=checkpoint.snapshot.get("summary", {}),
113
+ )
114
+
115
+ except Exception as e:
116
+ logger.error(f"Failed to create checkpoint: {e}", exc_info=True)
117
+ raise HTTPException(status_code=500, detail=str(e))
118
+
119
+
120
+ @router.get("", response_model=CheckpointListResponse)
121
+ @rate_limit_standard()
122
+ async def list_checkpoints(
123
+ request: Request,
124
+ limit: int = Query(50, ge=1, le=200),
125
+ workspace: Workspace = Depends(get_v2_workspace),
126
+ ) -> CheckpointListResponse:
127
+ """List all checkpoints.
128
+
129
+ This is the v2 equivalent of `cf checkpoint list`.
130
+
131
+ Args:
132
+ limit: Maximum checkpoints to return
133
+ workspace: v2 Workspace
134
+
135
+ Returns:
136
+ List of checkpoints (newest first)
137
+ """
138
+ checkpoint_list = checkpoints.list_all(workspace, limit=limit)
139
+
140
+ return CheckpointListResponse(
141
+ checkpoints=[
142
+ CheckpointResponse(
143
+ id=c.id,
144
+ name=c.name,
145
+ created_at=c.created_at.isoformat(),
146
+ summary=c.snapshot.get("summary", {}),
147
+ )
148
+ for c in checkpoint_list
149
+ ],
150
+ total=len(checkpoint_list),
151
+ )
152
+
153
+
154
+ @router.get("/{checkpoint_id}", response_model=CheckpointResponse)
155
+ @rate_limit_standard()
156
+ async def get_checkpoint(
157
+ request: Request,
158
+ checkpoint_id: str,
159
+ workspace: Workspace = Depends(get_v2_workspace),
160
+ ) -> CheckpointResponse:
161
+ """Get a specific checkpoint.
162
+
163
+ Args:
164
+ checkpoint_id: Checkpoint ID or name
165
+ workspace: v2 Workspace
166
+
167
+ Returns:
168
+ Checkpoint details
169
+
170
+ Raises:
171
+ HTTPException: 404 if checkpoint not found
172
+ """
173
+ checkpoint = checkpoints.get(workspace, checkpoint_id)
174
+ if not checkpoint:
175
+ raise HTTPException(
176
+ status_code=404,
177
+ detail=f"Checkpoint not found: {checkpoint_id}",
178
+ )
179
+
180
+ return CheckpointResponse(
181
+ id=checkpoint.id,
182
+ name=checkpoint.name,
183
+ created_at=checkpoint.created_at.isoformat(),
184
+ summary=checkpoint.snapshot.get("summary", {}),
185
+ )
186
+
187
+
188
+ @router.post("/{checkpoint_id}/restore")
189
+ @rate_limit_standard()
190
+ async def restore_checkpoint(
191
+ request: Request,
192
+ checkpoint_id: str,
193
+ workspace: Workspace = Depends(get_v2_workspace),
194
+ ) -> dict[str, Any]:
195
+ """Restore state from a checkpoint.
196
+
197
+ Restores task statuses from the checkpoint. Does not modify files.
198
+
199
+ This is the v2 equivalent of `cf checkpoint restore`.
200
+
201
+ Args:
202
+ checkpoint_id: Checkpoint ID or name
203
+ workspace: v2 Workspace
204
+
205
+ Returns:
206
+ Restore result
207
+
208
+ Raises:
209
+ HTTPException: 404 if checkpoint not found
210
+ """
211
+ try:
212
+ checkpoint = checkpoints.restore(workspace, checkpoint_id)
213
+ return {
214
+ "success": True,
215
+ "checkpoint_id": checkpoint.id,
216
+ "checkpoint_name": checkpoint.name,
217
+ "message": f"Restored state from checkpoint '{checkpoint.name}'",
218
+ }
219
+ except ValueError as e:
220
+ raise HTTPException(status_code=404, detail=str(e))
221
+ except Exception as e:
222
+ logger.error(f"Failed to restore checkpoint: {e}", exc_info=True)
223
+ raise HTTPException(status_code=500, detail=str(e))
224
+
225
+
226
+ @router.delete("/{checkpoint_id}")
227
+ @rate_limit_standard()
228
+ async def delete_checkpoint(
229
+ request: Request,
230
+ checkpoint_id: str,
231
+ workspace: Workspace = Depends(get_v2_workspace),
232
+ ) -> dict[str, Any]:
233
+ """Delete a checkpoint.
234
+
235
+ Args:
236
+ checkpoint_id: Checkpoint ID or name
237
+ workspace: v2 Workspace
238
+
239
+ Returns:
240
+ Delete result
241
+
242
+ Raises:
243
+ HTTPException: 404 if checkpoint not found
244
+ """
245
+ deleted = checkpoints.delete(workspace, checkpoint_id)
246
+ if not deleted:
247
+ raise HTTPException(
248
+ status_code=404,
249
+ detail=f"Checkpoint not found: {checkpoint_id}",
250
+ )
251
+
252
+ return {
253
+ "success": True,
254
+ "message": f"Deleted checkpoint {checkpoint_id}",
255
+ }
256
+
257
+
258
+ @router.get("/{checkpoint_id_a}/diff/{checkpoint_id_b}", response_model=CheckpointDiffResponse)
259
+ @rate_limit_standard()
260
+ async def diff_checkpoints(
261
+ request: Request,
262
+ checkpoint_id_a: str,
263
+ checkpoint_id_b: str,
264
+ workspace: Workspace = Depends(get_v2_workspace),
265
+ ) -> CheckpointDiffResponse:
266
+ """Compare two checkpoints.
267
+
268
+ Shows task status changes between two checkpoints.
269
+
270
+ Args:
271
+ checkpoint_id_a: First checkpoint ID (typically older)
272
+ checkpoint_id_b: Second checkpoint ID (typically newer)
273
+ workspace: v2 Workspace
274
+
275
+ Returns:
276
+ Diff result with task changes
277
+
278
+ Raises:
279
+ HTTPException: 404 if either checkpoint not found
280
+ """
281
+ try:
282
+ diff_result = checkpoints.diff(workspace, checkpoint_id_a, checkpoint_id_b)
283
+
284
+ return CheckpointDiffResponse(
285
+ checkpoint_a={
286
+ "id": diff_result.checkpoint_a_id,
287
+ "name": diff_result.checkpoint_a_name,
288
+ },
289
+ checkpoint_b={
290
+ "id": diff_result.checkpoint_b_id,
291
+ "name": diff_result.checkpoint_b_name,
292
+ },
293
+ task_diffs=[
294
+ TaskDiffResponse(
295
+ task_id=td.task_id,
296
+ title=td.title,
297
+ old_status=td.old_status,
298
+ new_status=td.new_status,
299
+ change_type=td.change_type,
300
+ )
301
+ for td in diff_result.task_diffs
302
+ ],
303
+ summary=diff_result.summary,
304
+ )
305
+
306
+ except ValueError as e:
307
+ raise HTTPException(status_code=404, detail=str(e))
308
+ except Exception as e:
309
+ logger.error(f"Failed to diff checkpoints: {e}", exc_info=True)
310
+ raise HTTPException(status_code=500, detail=str(e))