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,417 @@
1
+ """V2 Discovery workflow router - delegates to core modules.
2
+
3
+ This module provides v2-style API endpoints for PRD discovery that delegate
4
+ to core/prd_discovery.py. It uses the v2 Workspace model and is designed
5
+ to work alongside the v1 discovery router during migration.
6
+
7
+ Key differences from v1:
8
+ - Uses Workspace (path-based) instead of project_id
9
+ - Delegates to core/prd_discovery functions
10
+ - Stateless session management via session_id
11
+ - No LeadAgent dependency
12
+
13
+ The v1 router (discovery.py) remains for backwards compatibility with
14
+ existing web UI until Phase 3 (Web UI Rebuild).
15
+ """
16
+
17
+ import logging
18
+ from typing import Any, Optional
19
+
20
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
21
+ from pydantic import BaseModel, Field
22
+
23
+ from codeframe.core.workspace import Workspace
24
+ from codeframe.lib.rate_limiter import rate_limit_ai, rate_limit_standard
25
+ from codeframe.core import prd_discovery, prd, tasks
26
+ from codeframe.core.prd_discovery import (
27
+ NoApiKeyError,
28
+ ValidationError,
29
+ IncompleteSessionError,
30
+ )
31
+ from codeframe.ui.dependencies import get_v2_workspace
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ router = APIRouter(prefix="/api/v2/discovery", tags=["discovery-v2"])
36
+
37
+
38
+ # ============================================================================
39
+ # Request/Response Models
40
+ # ============================================================================
41
+
42
+
43
+ class StartDiscoveryResponse(BaseModel):
44
+ """Response for starting a discovery session."""
45
+
46
+ session_id: str
47
+ state: str
48
+ question: dict[str, Any]
49
+
50
+
51
+ class AnswerRequest(BaseModel):
52
+ """Request for submitting an answer."""
53
+
54
+ answer: str = Field(..., min_length=1, max_length=10000)
55
+
56
+
57
+ class AnswerResponse(BaseModel):
58
+ """Response for submitting an answer."""
59
+
60
+ accepted: bool
61
+ feedback: str
62
+ follow_up: Optional[str] = None
63
+ is_complete: bool
64
+ next_question: Optional[dict[str, Any]] = None
65
+ coverage: Optional[dict[str, Any]] = None
66
+
67
+
68
+ class StatusResponse(BaseModel):
69
+ """Response for discovery status."""
70
+
71
+ state: str
72
+ session_id: Optional[str] = None
73
+ progress: dict[str, Any] = {}
74
+ current_question: Optional[dict[str, Any]] = None
75
+ error: Optional[str] = None
76
+
77
+
78
+ class GeneratePrdRequest(BaseModel):
79
+ """Request for PRD generation."""
80
+
81
+ template_id: Optional[str] = Field(
82
+ None,
83
+ description="PRD template to use (standard, lean, enterprise, etc.)",
84
+ )
85
+
86
+
87
+ class GeneratePrdResponse(BaseModel):
88
+ """Response for PRD generation."""
89
+
90
+ prd_id: str
91
+ title: str
92
+ preview: str
93
+
94
+
95
+ class GenerateTasksResponse(BaseModel):
96
+ """Response for task generation."""
97
+
98
+ task_count: int
99
+ tasks: list[dict[str, Any]]
100
+
101
+
102
+ # ============================================================================
103
+ # Discovery Endpoints
104
+ # ============================================================================
105
+
106
+
107
+ @router.post("/start", response_model=StartDiscoveryResponse)
108
+ @rate_limit_ai()
109
+ async def start_discovery(
110
+ request: Request,
111
+ workspace: Workspace = Depends(get_v2_workspace),
112
+ ) -> StartDiscoveryResponse:
113
+ """Start a new PRD discovery session.
114
+
115
+ Creates a new AI-driven discovery session that will ask context-sensitive
116
+ questions to gather project requirements.
117
+
118
+ Args:
119
+ workspace: v2 Workspace (from workspace_path query param or default)
120
+
121
+ Returns:
122
+ Session ID, state, and first question
123
+
124
+ Raises:
125
+ HTTPException:
126
+ - 400: Discovery session already active
127
+ - 500: API key not configured or processing error
128
+ """
129
+ try:
130
+ # Check for existing active session
131
+ existing = prd_discovery.get_active_session(workspace)
132
+ if existing and not existing.is_complete():
133
+ raise HTTPException(
134
+ status_code=400,
135
+ detail={
136
+ "error": "Discovery session already active",
137
+ "session_id": existing.session_id,
138
+ "answered_count": existing.answered_count,
139
+ "hint": "Use POST /api/v2/discovery/{session_id}/answer to continue",
140
+ },
141
+ )
142
+
143
+ # Start new session
144
+ session = prd_discovery.start_discovery_session(workspace)
145
+ question = session.get_current_question()
146
+
147
+ return StartDiscoveryResponse(
148
+ session_id=session.session_id,
149
+ state=session.state.value,
150
+ question=question or {},
151
+ )
152
+
153
+ except NoApiKeyError as e:
154
+ raise HTTPException(
155
+ status_code=500,
156
+ detail=f"ANTHROPIC_API_KEY not configured: {e}",
157
+ )
158
+ except Exception as e:
159
+ logger.error(f"Failed to start discovery: {e}", exc_info=True)
160
+ raise HTTPException(
161
+ status_code=500,
162
+ detail=f"Failed to start discovery: {e}",
163
+ )
164
+
165
+
166
+ @router.get("/status", response_model=StatusResponse)
167
+ @rate_limit_standard()
168
+ async def get_status(
169
+ request: Request,
170
+ session_id: Optional[str] = Query(None, description="Specific session ID"),
171
+ workspace: Workspace = Depends(get_v2_workspace),
172
+ ) -> StatusResponse:
173
+ """Get discovery status for the workspace.
174
+
175
+ Returns status for a specific session (if provided) or the most recent
176
+ active session. If no active session exists, returns idle state.
177
+
178
+ Args:
179
+ session_id: Optional specific session ID to query
180
+ workspace: v2 Workspace
181
+
182
+ Returns:
183
+ Discovery status including state, progress, and current question
184
+ """
185
+ status = prd_discovery.get_discovery_status(
186
+ workspace,
187
+ session_id=session_id,
188
+ )
189
+ return StatusResponse(**status)
190
+
191
+
192
+ @router.post("/{session_id}/answer", response_model=AnswerResponse)
193
+ @rate_limit_ai()
194
+ async def submit_answer(
195
+ request: Request,
196
+ session_id: str,
197
+ body: AnswerRequest,
198
+ workspace: Workspace = Depends(get_v2_workspace),
199
+ ) -> AnswerResponse:
200
+ """Submit an answer to the current discovery question.
201
+
202
+ The AI validates the answer for adequacy. If adequate, the answer is
203
+ recorded and the next question (or completion) is returned. If not
204
+ adequate, feedback is provided with optional follow-up question.
205
+
206
+ Args:
207
+ request: HTTP request for rate limiting
208
+ session_id: Discovery session ID
209
+ body: Answer request with answer text
210
+ workspace: v2 Workspace
211
+
212
+ Returns:
213
+ Answer response with acceptance status and next steps
214
+
215
+ Raises:
216
+ HTTPException:
217
+ - 400: Validation error or session not active
218
+ - 404: Session not found
219
+ - 500: Processing error
220
+ """
221
+ try:
222
+ result = prd_discovery.process_discovery_answer(
223
+ workspace,
224
+ session_id,
225
+ body.answer,
226
+ )
227
+ return AnswerResponse(**result)
228
+
229
+ except ValueError as e:
230
+ raise HTTPException(status_code=404, detail=str(e))
231
+ except ValidationError as e:
232
+ raise HTTPException(status_code=400, detail=str(e))
233
+ except NoApiKeyError as e:
234
+ raise HTTPException(status_code=500, detail=str(e))
235
+ except Exception as e:
236
+ logger.error(f"Failed to process answer: {e}", exc_info=True)
237
+ raise HTTPException(status_code=500, detail=str(e))
238
+
239
+
240
+ @router.post("/{session_id}/generate-prd", response_model=GeneratePrdResponse)
241
+ @rate_limit_ai()
242
+ async def generate_prd(
243
+ request: Request,
244
+ session_id: str,
245
+ body: GeneratePrdRequest = None,
246
+ workspace: Workspace = Depends(get_v2_workspace),
247
+ ) -> GeneratePrdResponse:
248
+ """Generate a PRD from a completed discovery session.
249
+
250
+ Synthesizes the discovery conversation into a structured PRD document
251
+ using the specified template (or default).
252
+
253
+ Args:
254
+ request: HTTP request for rate limiting
255
+ session_id: Discovery session ID (must be complete)
256
+ body: Optional template selection
257
+ workspace: v2 Workspace
258
+
259
+ Returns:
260
+ Generated PRD with ID, title, and preview
261
+
262
+ Raises:
263
+ HTTPException:
264
+ - 400: Discovery not complete
265
+ - 404: Session not found
266
+ - 500: Generation error
267
+ """
268
+ try:
269
+ template_id = body.template_id if body else None
270
+ prd_record = prd_discovery.generate_prd_from_discovery(
271
+ workspace,
272
+ session_id,
273
+ template_id=template_id,
274
+ )
275
+
276
+ preview = prd_record.content[:500]
277
+ if len(prd_record.content) > 500:
278
+ preview += "..."
279
+
280
+ return GeneratePrdResponse(
281
+ prd_id=prd_record.id,
282
+ title=prd_record.title,
283
+ preview=preview,
284
+ )
285
+
286
+ except ValueError as e:
287
+ raise HTTPException(status_code=404, detail=str(e))
288
+ except IncompleteSessionError as e:
289
+ raise HTTPException(
290
+ status_code=400,
291
+ detail=f"Discovery not complete: {e}",
292
+ )
293
+ except NoApiKeyError as e:
294
+ raise HTTPException(status_code=500, detail=str(e))
295
+ except Exception as e:
296
+ logger.error(f"Failed to generate PRD: {e}", exc_info=True)
297
+ raise HTTPException(status_code=500, detail=str(e))
298
+
299
+
300
+ @router.post("/reset")
301
+ @rate_limit_standard()
302
+ async def reset_discovery(
303
+ request: Request,
304
+ session_id: Optional[str] = Query(None, description="Specific session to reset"),
305
+ workspace: Workspace = Depends(get_v2_workspace),
306
+ ) -> dict[str, Any]:
307
+ """Reset discovery for the workspace.
308
+
309
+ Marks the specified session (or most recent active session) as complete/abandoned.
310
+ Does NOT delete PRDs or tasks.
311
+
312
+ Args:
313
+ session_id: Optional specific session to reset
314
+ workspace: v2 Workspace
315
+
316
+ Returns:
317
+ Success status and message
318
+ """
319
+ reset = prd_discovery.reset_discovery(workspace, session_id=session_id)
320
+
321
+ if reset:
322
+ return {
323
+ "success": True,
324
+ "message": "Discovery session reset. Start a new session with POST /api/v2/discovery/start",
325
+ }
326
+ else:
327
+ return {
328
+ "success": True,
329
+ "message": "No active discovery session to reset.",
330
+ }
331
+
332
+
333
+ # ============================================================================
334
+ # Task Generation Endpoints (from PRD)
335
+ # ============================================================================
336
+
337
+
338
+ @router.post("/generate-tasks", response_model=GenerateTasksResponse)
339
+ @rate_limit_ai()
340
+ async def generate_tasks_from_prd(
341
+ request: Request,
342
+ prd_id: Optional[str] = Query(
343
+ None,
344
+ description="PRD ID to generate tasks from (defaults to latest)",
345
+ ),
346
+ use_llm: bool = Query(
347
+ True,
348
+ description="Use LLM for intelligent task generation (vs simple extraction)",
349
+ ),
350
+ workspace: Workspace = Depends(get_v2_workspace),
351
+ ) -> GenerateTasksResponse:
352
+ """Generate tasks from a PRD.
353
+
354
+ Uses LLM to decompose the PRD into actionable development tasks.
355
+ Falls back to simple extraction if LLM is unavailable.
356
+
357
+ This is the v2 equivalent of `cf tasks generate`.
358
+
359
+ Args:
360
+ prd_id: Optional PRD ID (defaults to latest PRD)
361
+ use_llm: Whether to use LLM (default True)
362
+ workspace: v2 Workspace
363
+
364
+ Returns:
365
+ List of generated tasks
366
+
367
+ Raises:
368
+ HTTPException:
369
+ - 404: PRD not found
370
+ - 500: Generation error
371
+ """
372
+ try:
373
+ # Get PRD
374
+ if prd_id:
375
+ prd_record = prd.get_by_id(workspace, prd_id)
376
+ if not prd_record:
377
+ raise HTTPException(
378
+ status_code=404,
379
+ detail=f"PRD not found: {prd_id}",
380
+ )
381
+ else:
382
+ prd_record = prd.get_latest(workspace)
383
+ if not prd_record:
384
+ raise HTTPException(
385
+ status_code=404,
386
+ detail="No PRD found. Generate a PRD first with POST /api/v2/discovery/start",
387
+ )
388
+
389
+ # Generate tasks
390
+ generated_tasks = tasks.generate_from_prd(
391
+ workspace,
392
+ prd_record,
393
+ use_llm=use_llm,
394
+ )
395
+
396
+ return GenerateTasksResponse(
397
+ task_count=len(generated_tasks),
398
+ tasks=[
399
+ {
400
+ "id": t.id,
401
+ "title": t.title,
402
+ "description": t.description,
403
+ "status": t.status.value,
404
+ "priority": t.priority,
405
+ }
406
+ for t in generated_tasks
407
+ ],
408
+ )
409
+
410
+ except HTTPException:
411
+ raise
412
+ except Exception as e:
413
+ logger.error(f"Failed to generate tasks: {e}", exc_info=True)
414
+ raise HTTPException(
415
+ status_code=500,
416
+ detail=f"Failed to generate tasks: {e}",
417
+ )
@@ -0,0 +1,284 @@
1
+ """V2 Environment router - delegates to core/environment module.
2
+
3
+ This module provides v2-style API endpoints for environment validation
4
+ and tool management. Used to check if development tools are installed.
5
+
6
+ Routes:
7
+ GET /api/v2/env/check - Quick environment validation
8
+ GET /api/v2/env/doctor - Comprehensive diagnostics
9
+ POST /api/v2/env/install - Install a missing tool
10
+ """
11
+
12
+ import logging
13
+ from typing import Optional
14
+
15
+ from fastapi import APIRouter, Depends, HTTPException, Request
16
+ from pydantic import BaseModel, Field
17
+
18
+ from codeframe.core.workspace import Workspace
19
+ from codeframe.lib.rate_limiter import rate_limit_standard
20
+ from codeframe.core.environment import EnvironmentValidator, ValidationResult, ToolInfo
21
+ from codeframe.core.installer import ToolInstaller
22
+ from codeframe.ui.dependencies import get_v2_workspace
23
+ from codeframe.ui.response_models import api_error, ErrorCodes
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ router = APIRouter(prefix="/api/v2/env", tags=["environment-v2"])
28
+
29
+
30
+ # ============================================================================
31
+ # Request/Response Models
32
+ # ============================================================================
33
+
34
+
35
+ class ToolInfoResponse(BaseModel):
36
+ """Response for a single tool."""
37
+
38
+ name: str
39
+ path: Optional[str]
40
+ version: Optional[str]
41
+ status: str # ToolStatus value
42
+ is_available: bool
43
+
44
+
45
+ class ValidationResultResponse(BaseModel):
46
+ """Response for environment validation."""
47
+
48
+ project_type: str
49
+ detected_tools: dict[str, ToolInfoResponse]
50
+ missing_tools: list[str]
51
+ optional_missing: list[str]
52
+ health_score: float
53
+ health_percent: int
54
+ is_healthy: bool
55
+ recommendations: list[str]
56
+ warnings: list[str]
57
+ conflicts: list[str]
58
+
59
+
60
+ class InstallToolRequest(BaseModel):
61
+ """Request for installing a tool."""
62
+
63
+ tool_name: str = Field(..., min_length=1, description="Tool to install")
64
+
65
+
66
+ class InstallResultResponse(BaseModel):
67
+ """Response for tool installation."""
68
+
69
+ success: bool
70
+ tool_name: str
71
+ message: str
72
+ command_used: Optional[str]
73
+
74
+
75
+ # ============================================================================
76
+ # Helper Functions
77
+ # ============================================================================
78
+
79
+
80
+ def _tool_to_response(info: ToolInfo) -> ToolInfoResponse:
81
+ """Convert a ToolInfo to a ToolInfoResponse."""
82
+ return ToolInfoResponse(
83
+ name=info.name,
84
+ path=info.path,
85
+ version=info.version,
86
+ status=info.status.value,
87
+ is_available=info.is_available,
88
+ )
89
+
90
+
91
+ def _result_to_response(result: ValidationResult) -> ValidationResultResponse:
92
+ """Convert a ValidationResult to a ValidationResultResponse."""
93
+ return ValidationResultResponse(
94
+ project_type=result.project_type,
95
+ detected_tools={
96
+ name: _tool_to_response(info)
97
+ for name, info in result.detected_tools.items()
98
+ },
99
+ missing_tools=result.missing_tools,
100
+ optional_missing=result.optional_missing,
101
+ health_score=result.health_score,
102
+ health_percent=int(result.health_score * 100),
103
+ is_healthy=result.is_healthy,
104
+ recommendations=result.recommendations,
105
+ warnings=result.warnings,
106
+ conflicts=result.conflicts,
107
+ )
108
+
109
+
110
+ # ============================================================================
111
+ # Endpoints
112
+ # ============================================================================
113
+
114
+
115
+ @router.get("/check", response_model=ValidationResultResponse)
116
+ @rate_limit_standard()
117
+ async def check_environment(
118
+ request: Request,
119
+ workspace: Workspace = Depends(get_v2_workspace),
120
+ ) -> ValidationResultResponse:
121
+ """Quick environment validation.
122
+
123
+ Checks if required tools are installed and returns health score.
124
+
125
+ Args:
126
+ workspace: v2 Workspace
127
+
128
+ Returns:
129
+ Validation result with tool status and health score
130
+ """
131
+ try:
132
+ validator = EnvironmentValidator()
133
+ result = validator.validate_environment(workspace.repo_path)
134
+
135
+ return _result_to_response(result)
136
+
137
+ except Exception as e:
138
+ logger.error(f"Failed to validate environment: {e}", exc_info=True)
139
+ raise HTTPException(
140
+ status_code=500,
141
+ detail=api_error("Validation failed", ErrorCodes.EXECUTION_FAILED, str(e)),
142
+ )
143
+
144
+
145
+ @router.get("/doctor", response_model=ValidationResultResponse)
146
+ @rate_limit_standard()
147
+ async def run_doctor(
148
+ request: Request,
149
+ workspace: Workspace = Depends(get_v2_workspace),
150
+ ) -> ValidationResultResponse:
151
+ """Comprehensive environment diagnostics.
152
+
153
+ Performs deeper validation including optional tools, version compatibility,
154
+ and provides detailed recommendations.
155
+
156
+ Args:
157
+ workspace: v2 Workspace
158
+
159
+ Returns:
160
+ Detailed validation result with recommendations
161
+ """
162
+ try:
163
+ validator = EnvironmentValidator()
164
+
165
+ # Doctor mode validates everything including optional tools
166
+ result = validator.validate_environment(
167
+ workspace.repo_path,
168
+ # Include optional tools in the check
169
+ optional_tools=["ruff", "black", "mypy", "pre-commit"],
170
+ )
171
+
172
+ return _result_to_response(result)
173
+
174
+ except Exception as e:
175
+ logger.error(f"Failed to run doctor: {e}", exc_info=True)
176
+ raise HTTPException(
177
+ status_code=500,
178
+ detail=api_error("Doctor failed", ErrorCodes.EXECUTION_FAILED, str(e)),
179
+ )
180
+
181
+
182
+ @router.post("/install", response_model=InstallResultResponse)
183
+ @rate_limit_standard()
184
+ async def install_tool(
185
+ request: Request,
186
+ body: InstallToolRequest,
187
+ workspace: Workspace = Depends(get_v2_workspace),
188
+ ) -> InstallResultResponse:
189
+ """Install a missing tool.
190
+
191
+ Attempts to install the specified tool using the appropriate
192
+ package manager for the current environment.
193
+
194
+ Args:
195
+ request: Install request with tool name
196
+ workspace: v2 Workspace
197
+
198
+ Returns:
199
+ Installation result
200
+
201
+ Raises:
202
+ HTTPException: 400 if tool unknown or installation fails
203
+ """
204
+ try:
205
+ installer = ToolInstaller()
206
+
207
+ # Check if tool is known
208
+ if not installer.can_install(body.tool_name):
209
+ raise HTTPException(
210
+ status_code=400,
211
+ detail=api_error(
212
+ "Unknown tool",
213
+ ErrorCodes.INVALID_REQUEST,
214
+ f"Don't know how to install '{body.tool_name}'",
215
+ ),
216
+ )
217
+
218
+ # Attempt installation (confirm=False for non-interactive server usage)
219
+ result = installer.install_tool(body.tool_name, confirm=False)
220
+
221
+ return InstallResultResponse(
222
+ success=result.success,
223
+ tool_name=result.tool_name,
224
+ message=result.message,
225
+ command_used=result.command_used,
226
+ )
227
+
228
+ except HTTPException:
229
+ raise
230
+ except Exception as e:
231
+ logger.error(f"Failed to install {body.tool_name}: {e}", exc_info=True)
232
+ raise HTTPException(
233
+ status_code=500,
234
+ detail=api_error("Installation failed", ErrorCodes.EXECUTION_FAILED, str(e)),
235
+ )
236
+
237
+
238
+ @router.get("/tools")
239
+ @rate_limit_standard()
240
+ async def list_available_tools(
241
+ request: Request,
242
+ workspace: Workspace = Depends(get_v2_workspace),
243
+ ) -> dict:
244
+ """List tools that can be automatically installed.
245
+
246
+ Args:
247
+ workspace: v2 Workspace
248
+
249
+ Returns:
250
+ List of installable tools with their package managers
251
+ """
252
+ try:
253
+ # List of tools known to be installable by ToolInstaller
254
+ # These are aggregated from PipInstaller, NpmInstaller, CargoInstaller, SystemInstaller
255
+ installable_tools = [
256
+ # Python tools (pip/uv)
257
+ {"name": "pytest", "category": "python", "description": "Python testing framework"},
258
+ {"name": "ruff", "category": "python", "description": "Fast Python linter"},
259
+ {"name": "mypy", "category": "python", "description": "Static type checker"},
260
+ {"name": "black", "category": "python", "description": "Code formatter"},
261
+ {"name": "flake8", "category": "python", "description": "Linting tool"},
262
+ {"name": "pylint", "category": "python", "description": "Code analyzer"},
263
+ {"name": "bandit", "category": "python", "description": "Security linter"},
264
+ {"name": "pre-commit", "category": "python", "description": "Git hooks manager"},
265
+ # Node.js tools (npm)
266
+ {"name": "eslint", "category": "nodejs", "description": "JavaScript linter"},
267
+ {"name": "prettier", "category": "nodejs", "description": "Code formatter"},
268
+ {"name": "typescript", "category": "nodejs", "description": "TypeScript compiler"},
269
+ # Rust tools (cargo)
270
+ {"name": "rustfmt", "category": "rust", "description": "Rust formatter"},
271
+ {"name": "clippy", "category": "rust", "description": "Rust linter"},
272
+ ]
273
+
274
+ return {
275
+ "tools": installable_tools,
276
+ "message": "Use POST /api/v2/env/install to install any of these tools",
277
+ }
278
+
279
+ except Exception as e:
280
+ logger.error(f"Failed to list tools: {e}", exc_info=True)
281
+ raise HTTPException(
282
+ status_code=500,
283
+ detail=api_error("Failed to list tools", ErrorCodes.EXECUTION_FAILED, str(e)),
284
+ )