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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- 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
|
+
)
|