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
codeframe/core/models.py
ADDED
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
"""Core data models for CodeFRAME."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Optional, Dict, Any, Literal, Union
|
|
7
|
+
from pydantic import BaseModel, Field, ConfigDict, computed_field, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskStatus(Enum):
|
|
11
|
+
"""Task execution status."""
|
|
12
|
+
|
|
13
|
+
PENDING = "pending"
|
|
14
|
+
ASSIGNED = "assigned"
|
|
15
|
+
IN_PROGRESS = "in_progress"
|
|
16
|
+
BLOCKED = "blocked"
|
|
17
|
+
COMPLETED = "completed"
|
|
18
|
+
FAILED = "failed"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Valid task status values for API validation and database constraints
|
|
22
|
+
VALID_TASK_STATUSES: frozenset[str] = frozenset(s.value for s in TaskStatus)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AgentMaturity(Enum):
|
|
26
|
+
"""Agent maturity levels based on Situational Leadership II."""
|
|
27
|
+
|
|
28
|
+
D1 = "directive" # Low skill, needs step-by-step
|
|
29
|
+
D2 = "coaching" # Some skill, needs guidance
|
|
30
|
+
D3 = "supporting" # High skill, needs autonomy
|
|
31
|
+
D4 = "delegating" # Expert, full ownership
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProjectStatus(Enum):
|
|
35
|
+
"""Project lifecycle status."""
|
|
36
|
+
|
|
37
|
+
INIT = "init"
|
|
38
|
+
PLANNING = "planning"
|
|
39
|
+
RUNNING = "running" # cf-10: Agent actively working on project
|
|
40
|
+
ACTIVE = "active"
|
|
41
|
+
PAUSED = "paused"
|
|
42
|
+
STOPPED = "stopped" # Agent terminated, project not active
|
|
43
|
+
COMPLETED = "completed"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ProjectPhase(Enum):
|
|
47
|
+
"""Project workflow phase."""
|
|
48
|
+
|
|
49
|
+
DISCOVERY = "discovery"
|
|
50
|
+
PLANNING = "planning"
|
|
51
|
+
ACTIVE = "active"
|
|
52
|
+
REVIEW = "review"
|
|
53
|
+
COMPLETE = "complete"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SourceType(Enum):
|
|
57
|
+
"""Project source type."""
|
|
58
|
+
|
|
59
|
+
GIT_REMOTE = "git_remote"
|
|
60
|
+
LOCAL_PATH = "local_path"
|
|
61
|
+
UPLOAD = "upload"
|
|
62
|
+
EMPTY = "empty"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Project:
|
|
67
|
+
"""Represents a CodeFRAME project.
|
|
68
|
+
|
|
69
|
+
Projects are the top-level container for issues and tasks. Each project
|
|
70
|
+
has a managed workspace, optional source tracking, and workflow state.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
id: Optional[int] = None
|
|
74
|
+
name: str = ""
|
|
75
|
+
description: str = ""
|
|
76
|
+
source_type: SourceType = SourceType.EMPTY
|
|
77
|
+
source_location: Optional[str] = None
|
|
78
|
+
source_branch: str = "main"
|
|
79
|
+
workspace_path: str = ""
|
|
80
|
+
git_initialized: bool = False
|
|
81
|
+
current_commit: Optional[str] = None
|
|
82
|
+
status: ProjectStatus = ProjectStatus.INIT
|
|
83
|
+
phase: ProjectPhase = ProjectPhase.DISCOVERY
|
|
84
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
85
|
+
paused_at: Optional[datetime] = None
|
|
86
|
+
config: Optional[Dict[str, Any]] = None
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
89
|
+
"""Convert Project to dictionary for JSON serialization."""
|
|
90
|
+
return {
|
|
91
|
+
"id": self.id,
|
|
92
|
+
"name": self.name,
|
|
93
|
+
"description": self.description,
|
|
94
|
+
"source_type": self.source_type.value,
|
|
95
|
+
"source_location": self.source_location,
|
|
96
|
+
"source_branch": self.source_branch,
|
|
97
|
+
"workspace_path": self.workspace_path,
|
|
98
|
+
"git_initialized": self.git_initialized,
|
|
99
|
+
"current_commit": self.current_commit,
|
|
100
|
+
"status": self.status.value,
|
|
101
|
+
"phase": self.phase.value,
|
|
102
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
103
|
+
"paused_at": self.paused_at.isoformat() if self.paused_at else None,
|
|
104
|
+
"config": self.config,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class BlockerSeverity(Enum):
|
|
109
|
+
"""Blocker severity for escalation (deprecated, use BlockerType)."""
|
|
110
|
+
|
|
111
|
+
SYNC = "sync" # Urgent, needs immediate response
|
|
112
|
+
ASYNC = "async" # Can wait, stack for later
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BlockerType(str, Enum):
|
|
116
|
+
"""Type of blocker requiring human intervention."""
|
|
117
|
+
|
|
118
|
+
SYNC = "SYNC" # Critical blocker - agent pauses immediately
|
|
119
|
+
ASYNC = "ASYNC" # Clarification request - agent continues work
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class BlockerStatus(str, Enum):
|
|
123
|
+
"""Current status of a blocker."""
|
|
124
|
+
|
|
125
|
+
PENDING = "PENDING" # Awaiting user response
|
|
126
|
+
RESOLVED = "RESOLVED" # User has provided answer
|
|
127
|
+
EXPIRED = "EXPIRED" # Blocker timed out (24h default)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Severity(str, Enum):
|
|
131
|
+
"""Code review finding severity levels (Sprint 10)."""
|
|
132
|
+
|
|
133
|
+
CRITICAL = "critical" # Must fix before completion
|
|
134
|
+
HIGH = "high" # Should fix before completion
|
|
135
|
+
MEDIUM = "medium" # Should fix eventually
|
|
136
|
+
LOW = "low" # Nice to fix
|
|
137
|
+
INFO = "info" # Informational only
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ReviewCategory(str, Enum):
|
|
141
|
+
"""Code review finding categories (Sprint 10)."""
|
|
142
|
+
|
|
143
|
+
SECURITY = "security" # Security vulnerabilities
|
|
144
|
+
PERFORMANCE = "performance" # Performance issues
|
|
145
|
+
QUALITY = "quality" # Code quality problems
|
|
146
|
+
MAINTAINABILITY = "maintainability" # Hard to maintain code
|
|
147
|
+
STYLE = "style" # Style/formatting issues
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class QualityGateType(str, Enum):
|
|
151
|
+
"""Types of quality gates (Sprint 10)."""
|
|
152
|
+
|
|
153
|
+
BUILD = "build"
|
|
154
|
+
TESTS = "tests"
|
|
155
|
+
TYPE_CHECK = "type_check"
|
|
156
|
+
COVERAGE = "coverage"
|
|
157
|
+
CODE_REVIEW = "code_review"
|
|
158
|
+
LINTING = "linting"
|
|
159
|
+
SKIP_DETECTION = "skip_detection"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CallType(str, Enum):
|
|
163
|
+
"""Type of LLM call for categorization (Sprint 10)."""
|
|
164
|
+
|
|
165
|
+
TASK_EXECUTION = "task_execution"
|
|
166
|
+
CODE_REVIEW = "code_review"
|
|
167
|
+
COORDINATION = "coordination"
|
|
168
|
+
OTHER = "other"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class Issue:
|
|
173
|
+
"""Represents a high-level work item that contains multiple tasks.
|
|
174
|
+
|
|
175
|
+
Issues are numbered hierarchically (e.g., "1.5", "2.3") and can parallelize
|
|
176
|
+
with other issues at the same level. Each issue contains sequential tasks.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
id: Optional[int] = None
|
|
180
|
+
project_id: Optional[int] = None
|
|
181
|
+
issue_number: str = "" # e.g., "1.5" or "2.1"
|
|
182
|
+
title: str = ""
|
|
183
|
+
description: str = ""
|
|
184
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
185
|
+
priority: int = 2 # 0-4, 0 = highest
|
|
186
|
+
workflow_step: int = 1
|
|
187
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
188
|
+
completed_at: Optional[datetime] = None
|
|
189
|
+
|
|
190
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
191
|
+
"""Convert Issue to dictionary for JSON serialization."""
|
|
192
|
+
return {
|
|
193
|
+
"id": self.id,
|
|
194
|
+
"project_id": self.project_id,
|
|
195
|
+
"issue_number": self.issue_number,
|
|
196
|
+
"title": self.title,
|
|
197
|
+
"description": self.description,
|
|
198
|
+
"status": self.status.value,
|
|
199
|
+
"priority": self.priority,
|
|
200
|
+
"workflow_step": self.workflow_step,
|
|
201
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
202
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class IssueWithTaskCount:
|
|
208
|
+
"""Issue with associated task count for summary views.
|
|
209
|
+
|
|
210
|
+
Uses composition to wrap an Issue and add task_count, reducing
|
|
211
|
+
field duplication and ensuring changes to Issue are automatically
|
|
212
|
+
reflected here.
|
|
213
|
+
|
|
214
|
+
Used by get_issue_with_task_counts() to return typed results
|
|
215
|
+
instead of raw dictionaries.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
issue: Issue
|
|
219
|
+
task_count: int = 0
|
|
220
|
+
|
|
221
|
+
# Convenience accessors for common Issue fields
|
|
222
|
+
@property
|
|
223
|
+
def id(self) -> Optional[int]:
|
|
224
|
+
return self.issue.id
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def project_id(self) -> Optional[int]:
|
|
228
|
+
return self.issue.project_id
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def issue_number(self) -> str:
|
|
232
|
+
return self.issue.issue_number
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def title(self) -> str:
|
|
236
|
+
return self.issue.title
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def status(self) -> TaskStatus:
|
|
240
|
+
return self.issue.status
|
|
241
|
+
|
|
242
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
243
|
+
"""Convert to dictionary for JSON serialization."""
|
|
244
|
+
result = self.issue.to_dict()
|
|
245
|
+
result["task_count"] = self.task_count
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class Task:
|
|
251
|
+
"""Represents an atomic development task within an issue.
|
|
252
|
+
|
|
253
|
+
Tasks are numbered hierarchically (e.g., "1.5.1", "1.5.2") and are
|
|
254
|
+
always sequential within their parent issue (cannot parallelize).
|
|
255
|
+
Each task depends on the previous task in the sequence.
|
|
256
|
+
|
|
257
|
+
Effort estimation fields support project planning and scheduling:
|
|
258
|
+
- estimated_hours: Time estimate in hours for task completion
|
|
259
|
+
- complexity_score: Task complexity rating (1-5 scale)
|
|
260
|
+
- uncertainty_level: Confidence in estimate ("low", "medium", "high")
|
|
261
|
+
- resource_requirements: JSON string of required skills/tools
|
|
262
|
+
|
|
263
|
+
Supervisor intervention context (set when retrying after tactical pattern match):
|
|
264
|
+
- intervention_context: Dict with structure:
|
|
265
|
+
{
|
|
266
|
+
"intervention_applied": bool, # True if supervisor applied intervention
|
|
267
|
+
"pattern_matched": str, # ID of matched tactical pattern
|
|
268
|
+
"existing_files": List[str], # Files that already exist
|
|
269
|
+
"instruction": str, # Guidance for agent on retry
|
|
270
|
+
"strategy": str, # InterventionStrategy value
|
|
271
|
+
}
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
id: Optional[int] = None
|
|
275
|
+
project_id: Optional[int] = None
|
|
276
|
+
issue_id: Optional[int] = None # Foreign key to parent issue
|
|
277
|
+
task_number: str = "" # e.g., "1.5.3"
|
|
278
|
+
parent_issue_number: str = "" # e.g., "1.5"
|
|
279
|
+
title: str = ""
|
|
280
|
+
description: str = ""
|
|
281
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
282
|
+
assigned_to: Optional[str] = None
|
|
283
|
+
depends_on: str = "" # Previous task number (e.g., "1.5.2")
|
|
284
|
+
can_parallelize: bool = False # Always FALSE within issue
|
|
285
|
+
priority: int = 2 # 0-4, 0 = highest (inherited from issue)
|
|
286
|
+
workflow_step: int = 1 # Maps to 15-step workflow
|
|
287
|
+
requires_mcp: bool = False
|
|
288
|
+
estimated_tokens: int = 0
|
|
289
|
+
actual_tokens: Optional[int] = None
|
|
290
|
+
# Effort estimation fields
|
|
291
|
+
estimated_hours: Optional[float] = None # Time estimate in hours
|
|
292
|
+
complexity_score: Optional[int] = None # Complexity rating (1-5)
|
|
293
|
+
uncertainty_level: Optional[str] = None # "low", "medium", "high"
|
|
294
|
+
resource_requirements: Optional[str] = None # JSON string of required skills/tools
|
|
295
|
+
# Supervisor intervention context (populated on retry after tactical pattern match)
|
|
296
|
+
intervention_context: Optional[Dict[str, Any]] = None
|
|
297
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
298
|
+
completed_at: Optional[datetime] = None
|
|
299
|
+
|
|
300
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
301
|
+
"""Convert Task to dictionary for JSON serialization."""
|
|
302
|
+
return {
|
|
303
|
+
"id": self.id,
|
|
304
|
+
"project_id": self.project_id,
|
|
305
|
+
"issue_id": self.issue_id,
|
|
306
|
+
"task_number": self.task_number,
|
|
307
|
+
"parent_issue_number": self.parent_issue_number,
|
|
308
|
+
"title": self.title,
|
|
309
|
+
"description": self.description,
|
|
310
|
+
"status": self.status.value,
|
|
311
|
+
"assigned_to": self.assigned_to,
|
|
312
|
+
"depends_on": self.depends_on,
|
|
313
|
+
"can_parallelize": self.can_parallelize,
|
|
314
|
+
"priority": self.priority,
|
|
315
|
+
"workflow_step": self.workflow_step,
|
|
316
|
+
"requires_mcp": self.requires_mcp,
|
|
317
|
+
"estimated_tokens": self.estimated_tokens,
|
|
318
|
+
"actual_tokens": self.actual_tokens,
|
|
319
|
+
"estimated_hours": self.estimated_hours,
|
|
320
|
+
"complexity_score": self.complexity_score,
|
|
321
|
+
"uncertainty_level": self.uncertainty_level,
|
|
322
|
+
"resource_requirements": self.resource_requirements,
|
|
323
|
+
"intervention_context": self.intervention_context,
|
|
324
|
+
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
325
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass
|
|
330
|
+
class Blocker:
|
|
331
|
+
"""Represents a task blocker requiring human input."""
|
|
332
|
+
|
|
333
|
+
id: int
|
|
334
|
+
task_id: int
|
|
335
|
+
severity: BlockerSeverity
|
|
336
|
+
reason: str
|
|
337
|
+
question: str
|
|
338
|
+
resolution: Optional[str] = None
|
|
339
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
340
|
+
resolved_at: Optional[datetime] = None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@dataclass
|
|
344
|
+
class AgentMetrics:
|
|
345
|
+
"""Performance metrics for agent maturity assessment."""
|
|
346
|
+
|
|
347
|
+
task_success_rate: float
|
|
348
|
+
blocker_frequency: float
|
|
349
|
+
test_pass_rate: float
|
|
350
|
+
rework_rate: float
|
|
351
|
+
context_efficiency: float # tokens per task
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass
|
|
355
|
+
class Notification:
|
|
356
|
+
"""Multi-channel notification."""
|
|
357
|
+
|
|
358
|
+
id: str
|
|
359
|
+
project_id: int
|
|
360
|
+
severity: BlockerSeverity
|
|
361
|
+
title: str
|
|
362
|
+
message: str
|
|
363
|
+
blocker_id: Optional[int] = None
|
|
364
|
+
action_required: str = ""
|
|
365
|
+
channels: List[str] = field(default_factory=list)
|
|
366
|
+
sent_at: Optional[datetime] = None
|
|
367
|
+
acknowledged_at: Optional[datetime] = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# Pydantic models for API validation and serialization
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class BlockerModel(BaseModel):
|
|
374
|
+
"""Pydantic model for blocker database records."""
|
|
375
|
+
|
|
376
|
+
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
|
377
|
+
|
|
378
|
+
id: int
|
|
379
|
+
agent_id: str
|
|
380
|
+
task_id: Optional[int] = None
|
|
381
|
+
blocker_type: BlockerType
|
|
382
|
+
question: str = Field(..., max_length=2000)
|
|
383
|
+
answer: Optional[str] = Field(None, max_length=5000)
|
|
384
|
+
status: BlockerStatus
|
|
385
|
+
created_at: datetime
|
|
386
|
+
resolved_at: Optional[datetime] = None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class BlockerCreate(BaseModel):
|
|
390
|
+
"""Request model for creating a blocker."""
|
|
391
|
+
|
|
392
|
+
agent_id: str
|
|
393
|
+
task_id: Optional[int] = None
|
|
394
|
+
blocker_type: BlockerType = BlockerType.ASYNC
|
|
395
|
+
question: str = Field(..., min_length=1, max_length=2000)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class BlockerResolve(BaseModel):
|
|
399
|
+
"""Request model for resolving a blocker."""
|
|
400
|
+
|
|
401
|
+
answer: str = Field(..., min_length=1, max_length=5000)
|
|
402
|
+
|
|
403
|
+
@field_validator("answer")
|
|
404
|
+
@classmethod
|
|
405
|
+
def validate_answer_not_whitespace(cls, v: str) -> str:
|
|
406
|
+
"""Validate that answer is not empty or whitespace-only."""
|
|
407
|
+
if not v.strip():
|
|
408
|
+
raise ValueError("Answer cannot be empty or whitespace-only")
|
|
409
|
+
return v
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class BlockerListResponse(BaseModel):
|
|
413
|
+
"""Response model for listing blockers."""
|
|
414
|
+
|
|
415
|
+
blockers: List[BlockerModel]
|
|
416
|
+
total: int
|
|
417
|
+
pending_count: int
|
|
418
|
+
sync_count: int
|
|
419
|
+
async_count: int = 0
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# Sprint 9: MVP Completion Models
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class LintResult(BaseModel):
|
|
426
|
+
"""Lint execution result.
|
|
427
|
+
|
|
428
|
+
Stores results from linting tools (ruff, eslint) for tracking quality trends
|
|
429
|
+
over time and enforcing quality gates.
|
|
430
|
+
|
|
431
|
+
Attributes:
|
|
432
|
+
id: Database ID (auto-generated)
|
|
433
|
+
task_id: Task that was linted
|
|
434
|
+
linter: Tool used (ruff, eslint, other)
|
|
435
|
+
error_count: Number of critical errors (block task completion)
|
|
436
|
+
warning_count: Number of non-blocking warnings
|
|
437
|
+
files_linted: Number of files checked
|
|
438
|
+
output: Full lint output (JSON or text)
|
|
439
|
+
created_at: When lint was executed
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
id: Optional[int] = None
|
|
443
|
+
task_id: Optional[int] = Field(None, description="Task ID (optional for in-memory results)")
|
|
444
|
+
linter: Literal["ruff", "eslint", "other"] = Field(..., description="Linter tool")
|
|
445
|
+
error_count: int = Field(0, ge=0, description="Number of errors")
|
|
446
|
+
warning_count: int = Field(0, ge=0, description="Number of warnings")
|
|
447
|
+
files_linted: int = Field(0, ge=0, description="Number of files checked")
|
|
448
|
+
output: Optional[str] = Field(None, description="Full lint output (JSON or text)")
|
|
449
|
+
created_at: Optional[datetime] = None
|
|
450
|
+
|
|
451
|
+
@property
|
|
452
|
+
def has_critical_errors(self) -> bool:
|
|
453
|
+
"""Check if lint found critical errors that block task."""
|
|
454
|
+
return self.error_count > 0
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def total_issues(self) -> int:
|
|
458
|
+
"""Total issues (errors + warnings)."""
|
|
459
|
+
return self.error_count + self.warning_count
|
|
460
|
+
|
|
461
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
462
|
+
"""Convert to dictionary for database storage."""
|
|
463
|
+
return {
|
|
464
|
+
"task_id": self.task_id,
|
|
465
|
+
"linter": self.linter,
|
|
466
|
+
"error_count": self.error_count,
|
|
467
|
+
"warning_count": self.warning_count,
|
|
468
|
+
"files_linted": self.files_linted,
|
|
469
|
+
"output": self.output,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class ReviewFinding(BaseModel):
|
|
474
|
+
"""Individual review finding.
|
|
475
|
+
|
|
476
|
+
Represents a single code quality issue found during code review
|
|
477
|
+
(complexity, security, style, or duplication).
|
|
478
|
+
|
|
479
|
+
Attributes:
|
|
480
|
+
category: Type of issue (complexity, security, style, duplication)
|
|
481
|
+
severity: Criticality level (critical, high, medium, low)
|
|
482
|
+
file_path: File containing the issue
|
|
483
|
+
line_number: Line number (optional)
|
|
484
|
+
message: Human-readable description
|
|
485
|
+
suggestion: Recommended fix (optional)
|
|
486
|
+
tool: Tool that detected issue (radon, bandit, etc.)
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
category: Literal["complexity", "security", "style", "duplication"] = Field(
|
|
490
|
+
..., description="Finding category"
|
|
491
|
+
)
|
|
492
|
+
severity: Literal["critical", "high", "medium", "low"] = Field(
|
|
493
|
+
..., description="Severity level"
|
|
494
|
+
)
|
|
495
|
+
file_path: str = Field(..., description="File with issue")
|
|
496
|
+
line_number: Optional[int] = Field(None, description="Line number (if applicable)")
|
|
497
|
+
message: str = Field(..., description="Human-readable description")
|
|
498
|
+
suggestion: Optional[str] = Field(None, description="Recommended fix")
|
|
499
|
+
tool: str = Field(..., description="Tool that detected issue (radon, bandit, etc.)")
|
|
500
|
+
|
|
501
|
+
def to_markdown(self) -> str:
|
|
502
|
+
"""Format finding as markdown for blocker display."""
|
|
503
|
+
severity_emoji = {
|
|
504
|
+
"critical": "🔴",
|
|
505
|
+
"high": "🟠",
|
|
506
|
+
"medium": "🟡",
|
|
507
|
+
"low": "⚪",
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
location = f"{self.file_path}:{self.line_number}" if self.line_number else self.file_path
|
|
511
|
+
|
|
512
|
+
md = f"{severity_emoji[self.severity]} **{self.severity.upper()}** [{self.category}] {location}\n"
|
|
513
|
+
md += f" {self.message}\n"
|
|
514
|
+
|
|
515
|
+
if self.suggestion:
|
|
516
|
+
md += f" 💡 Suggestion: {self.suggestion}\n"
|
|
517
|
+
|
|
518
|
+
return md
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
class ReviewReport(BaseModel):
|
|
522
|
+
"""Complete review report for a task.
|
|
523
|
+
|
|
524
|
+
Aggregates all review findings into a comprehensive quality assessment
|
|
525
|
+
with scoring and approve/reject decision.
|
|
526
|
+
|
|
527
|
+
Attributes:
|
|
528
|
+
task_id: Task that was reviewed
|
|
529
|
+
reviewer_agent_id: Agent that performed review
|
|
530
|
+
overall_score: Overall quality score (0-100)
|
|
531
|
+
complexity_score: Complexity subscore (0-100)
|
|
532
|
+
security_score: Security subscore (0-100)
|
|
533
|
+
style_score: Style subscore (0-100)
|
|
534
|
+
status: Review decision (approved, changes_requested, rejected)
|
|
535
|
+
findings: List of individual findings
|
|
536
|
+
summary: Human-readable summary
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
task_id: int
|
|
540
|
+
reviewer_agent_id: str
|
|
541
|
+
overall_score: float = Field(..., ge=0, le=100, description="Overall quality score (0-100)")
|
|
542
|
+
complexity_score: float = Field(..., ge=0, le=100)
|
|
543
|
+
security_score: float = Field(..., ge=0, le=100)
|
|
544
|
+
style_score: float = Field(..., ge=0, le=100)
|
|
545
|
+
status: Literal["approved", "changes_requested", "rejected"]
|
|
546
|
+
findings: List[ReviewFinding] = Field(default_factory=list)
|
|
547
|
+
summary: str = Field(..., description="Human-readable summary")
|
|
548
|
+
|
|
549
|
+
@property
|
|
550
|
+
def has_critical_findings(self) -> bool:
|
|
551
|
+
"""Check if any findings are critical severity."""
|
|
552
|
+
return any(f.severity == "critical" for f in self.findings)
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def critical_count(self) -> int:
|
|
556
|
+
"""Count critical findings."""
|
|
557
|
+
return sum(1 for f in self.findings if f.severity == "critical")
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def high_count(self) -> int:
|
|
561
|
+
"""Count high severity findings."""
|
|
562
|
+
return sum(1 for f in self.findings if f.severity == "high")
|
|
563
|
+
|
|
564
|
+
def to_blocker_message(self) -> str:
|
|
565
|
+
"""Format review report as blocker message."""
|
|
566
|
+
msg = f"## Code Review: {self.status.replace('_', ' ').title()}\n\n"
|
|
567
|
+
msg += f"**Overall Score**: {self.overall_score:.1f}/100\n\n"
|
|
568
|
+
|
|
569
|
+
if self.findings:
|
|
570
|
+
msg += f"**Findings**: {len(self.findings)} issues ({self.critical_count} critical, {self.high_count} high)\n\n"
|
|
571
|
+
|
|
572
|
+
# Group by severity
|
|
573
|
+
for severity in ["critical", "high", "medium", "low"]:
|
|
574
|
+
severity_findings = [f for f in self.findings if f.severity == severity]
|
|
575
|
+
if severity_findings:
|
|
576
|
+
msg += f"### {severity.upper()} Issues\n\n"
|
|
577
|
+
for finding in severity_findings:
|
|
578
|
+
msg += finding.to_markdown() + "\n"
|
|
579
|
+
|
|
580
|
+
msg += f"\n---\n\n{self.summary}"
|
|
581
|
+
|
|
582
|
+
return msg
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ============================================================================
|
|
586
|
+
# Discovery Answer UI Models (Feature: 012-discovery-answer-ui)
|
|
587
|
+
# ============================================================================
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
class DiscoveryAnswer(BaseModel):
|
|
591
|
+
"""Request model for discovery answer submission."""
|
|
592
|
+
|
|
593
|
+
model_config = ConfigDict(str_strip_whitespace=True)
|
|
594
|
+
|
|
595
|
+
answer: str = Field(
|
|
596
|
+
...,
|
|
597
|
+
min_length=1,
|
|
598
|
+
max_length=5000,
|
|
599
|
+
description="User's answer to the current discovery question",
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
@field_validator("answer")
|
|
603
|
+
@classmethod
|
|
604
|
+
def validate_answer(cls, v: str) -> str:
|
|
605
|
+
"""Ensure answer is not empty after trimming."""
|
|
606
|
+
trimmed = v.strip()
|
|
607
|
+
if not trimmed:
|
|
608
|
+
raise ValueError("Answer cannot be empty or whitespace only")
|
|
609
|
+
if len(trimmed) > 5000:
|
|
610
|
+
raise ValueError("Answer cannot exceed 5000 characters")
|
|
611
|
+
return trimmed
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class DiscoveryAnswerResponse(BaseModel):
|
|
615
|
+
"""Response model for discovery answer submission."""
|
|
616
|
+
|
|
617
|
+
success: bool = Field(..., description="Whether the answer was successfully processed")
|
|
618
|
+
|
|
619
|
+
next_question: Optional[str] = Field(
|
|
620
|
+
None, description="Next discovery question text (null if discovery complete)"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
is_complete: bool = Field(..., description="Whether the discovery phase is complete")
|
|
624
|
+
|
|
625
|
+
current_index: int = Field(..., ge=0, description="Current question index (0-based)")
|
|
626
|
+
|
|
627
|
+
total_questions: int = Field(..., gt=0, description="Total number of discovery questions")
|
|
628
|
+
|
|
629
|
+
progress_percentage: float = Field(
|
|
630
|
+
..., ge=0.0, le=100.0, description="Discovery completion percentage (0.0 - 100.0)"
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
model_config = ConfigDict(
|
|
634
|
+
json_schema_extra={
|
|
635
|
+
"example": {
|
|
636
|
+
"success": True,
|
|
637
|
+
"next_question": "What tech stack are you planning to use?",
|
|
638
|
+
"is_complete": False,
|
|
639
|
+
"current_index": 3,
|
|
640
|
+
"total_questions": 20,
|
|
641
|
+
"progress_percentage": 15.0,
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# ============================================================================
|
|
648
|
+
# Sprint 10 Models (Feature: 015-review-polish)
|
|
649
|
+
# ============================================================================
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class CodeReview(BaseModel):
|
|
653
|
+
"""Code review finding from Review Agent (Sprint 10)."""
|
|
654
|
+
|
|
655
|
+
id: Optional[int] = None
|
|
656
|
+
task_id: int
|
|
657
|
+
agent_id: str
|
|
658
|
+
project_id: int
|
|
659
|
+
file_path: str = Field(..., description="Relative path from project root")
|
|
660
|
+
line_number: Optional[int] = Field(None, description="Line number, None for file-level")
|
|
661
|
+
severity: Severity
|
|
662
|
+
category: ReviewCategory
|
|
663
|
+
message: str = Field(..., min_length=10, description="Description of the issue")
|
|
664
|
+
recommendation: Optional[str] = Field(None, description="How to fix it")
|
|
665
|
+
code_snippet: Optional[str] = Field(None, description="Offending code for context")
|
|
666
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
667
|
+
|
|
668
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def is_blocking(self) -> bool:
|
|
672
|
+
"""Whether this finding should block task completion."""
|
|
673
|
+
# Handle both enum and string values (use_enum_values=True converts to strings)
|
|
674
|
+
blocking_severities = [Severity.CRITICAL.value, Severity.HIGH.value]
|
|
675
|
+
severity_value = self.severity if isinstance(self.severity, str) else self.severity.value
|
|
676
|
+
return severity_value in blocking_severities
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class TokenUsage(BaseModel):
|
|
680
|
+
"""Token usage record for a single LLM call (Sprint 10)."""
|
|
681
|
+
|
|
682
|
+
id: Optional[int] = None
|
|
683
|
+
# Tasks use integer PKs in the v1 schema and UUID strings in v2 workspaces;
|
|
684
|
+
# SQLite is type-flexible, so we accept either at the model boundary.
|
|
685
|
+
task_id: Optional[Union[int, str]] = None # None for non-task calls
|
|
686
|
+
agent_id: str
|
|
687
|
+
project_id: int
|
|
688
|
+
model_name: str = Field(..., description="e.g., claude-sonnet-4-5")
|
|
689
|
+
input_tokens: int = Field(..., ge=0)
|
|
690
|
+
output_tokens: int = Field(..., ge=0)
|
|
691
|
+
estimated_cost_usd: float = Field(..., ge=0.0)
|
|
692
|
+
actual_cost_usd: Optional[float] = Field(None, ge=0.0)
|
|
693
|
+
call_type: CallType = CallType.OTHER
|
|
694
|
+
session_id: Optional[str] = Field(None, description="SDK session ID for conversation tracking")
|
|
695
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
696
|
+
|
|
697
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
698
|
+
|
|
699
|
+
@property
|
|
700
|
+
def total_tokens(self) -> int:
|
|
701
|
+
"""Total tokens (input + output)."""
|
|
702
|
+
return self.input_tokens + self.output_tokens
|
|
703
|
+
|
|
704
|
+
@staticmethod
|
|
705
|
+
def calculate_cost(model_name: str, input_tokens: int, output_tokens: int) -> float:
|
|
706
|
+
"""Calculate estimated cost in USD.
|
|
707
|
+
|
|
708
|
+
Pricing as of 2025-11:
|
|
709
|
+
- Claude Sonnet 4.5: $3.00 input / $15.00 output per MTok
|
|
710
|
+
- Claude Opus 4: $15.00 input / $75.00 output per MTok
|
|
711
|
+
- Claude Haiku 4: $0.80 input / $4.00 output per MTok
|
|
712
|
+
"""
|
|
713
|
+
pricing = {
|
|
714
|
+
"claude-sonnet-4-5": {"input": 3.00, "output": 15.00},
|
|
715
|
+
"claude-opus-4": {"input": 15.00, "output": 75.00},
|
|
716
|
+
"claude-haiku-4": {"input": 0.80, "output": 4.00},
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if model_name not in pricing:
|
|
720
|
+
raise ValueError(f"Unknown model: {model_name}")
|
|
721
|
+
|
|
722
|
+
prices = pricing[model_name]
|
|
723
|
+
cost = (input_tokens * prices["input"] / 1_000_000) + (
|
|
724
|
+
output_tokens * prices["output"] / 1_000_000
|
|
725
|
+
)
|
|
726
|
+
return round(cost, 6) # 6 decimal places for precision
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
class QualityGateFailure(BaseModel):
|
|
730
|
+
"""Individual quality gate failure (Sprint 10)."""
|
|
731
|
+
|
|
732
|
+
gate: QualityGateType
|
|
733
|
+
reason: str = Field(..., min_length=5)
|
|
734
|
+
details: Optional[str] = None # Full error output
|
|
735
|
+
severity: Severity = Severity.HIGH
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class QualityGateResult(BaseModel):
|
|
739
|
+
"""Result of running quality gates for a task (Sprint 10).
|
|
740
|
+
|
|
741
|
+
Includes information about which gates were run based on task classification,
|
|
742
|
+
and which gates were skipped as not applicable.
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
task_id: int
|
|
746
|
+
status: str = Field(..., description="passed or failed")
|
|
747
|
+
failures: List[QualityGateFailure] = Field(default_factory=list)
|
|
748
|
+
execution_time_seconds: float = Field(..., ge=0.0)
|
|
749
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
750
|
+
skipped_gates: List[str] = Field(
|
|
751
|
+
default_factory=list,
|
|
752
|
+
description="Gates that were skipped based on task classification"
|
|
753
|
+
)
|
|
754
|
+
task_category: Optional[str] = Field(
|
|
755
|
+
default=None,
|
|
756
|
+
description="Task category used for gate selection (e.g., 'design', 'code_implementation')"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
@property
|
|
760
|
+
def passed(self) -> bool:
|
|
761
|
+
"""Whether all gates passed."""
|
|
762
|
+
return self.status == "passed" and len(self.failures) == 0
|
|
763
|
+
|
|
764
|
+
@property
|
|
765
|
+
def has_critical_failures(self) -> bool:
|
|
766
|
+
"""Whether any failures are critical."""
|
|
767
|
+
return any(f.severity == Severity.CRITICAL for f in self.failures)
|
|
768
|
+
|
|
769
|
+
@property
|
|
770
|
+
def gates_skipped_count(self) -> int:
|
|
771
|
+
"""Number of gates that were skipped."""
|
|
772
|
+
return len(self.skipped_gates)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class CheckpointMetadata(BaseModel):
|
|
776
|
+
"""Metadata stored in checkpoint for quick inspection (Sprint 10)."""
|
|
777
|
+
|
|
778
|
+
project_id: int
|
|
779
|
+
phase: str # discovery, planning, active, review, complete
|
|
780
|
+
tasks_completed: int
|
|
781
|
+
tasks_total: int
|
|
782
|
+
agents_active: List[str]
|
|
783
|
+
last_task_completed: Optional[str] = None
|
|
784
|
+
context_items_count: int
|
|
785
|
+
total_cost_usd: float
|
|
786
|
+
# Quality tracking fields (optional for backward compatibility)
|
|
787
|
+
quality_stats: Optional[Dict[str, Any]] = None # Current quality metrics from QualityTracker
|
|
788
|
+
quality_trend: Optional[str] = None # "improving", "stable", or "declining"
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
class Checkpoint(BaseModel):
|
|
792
|
+
"""Project checkpoint for restore operations (Sprint 10)."""
|
|
793
|
+
|
|
794
|
+
id: Optional[int] = None
|
|
795
|
+
project_id: int
|
|
796
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
797
|
+
description: Optional[str] = Field(None, max_length=500)
|
|
798
|
+
trigger: str = Field(..., description="manual, auto, phase_transition, pause")
|
|
799
|
+
git_commit: str = Field(..., min_length=7, max_length=40, description="Git commit SHA")
|
|
800
|
+
database_backup_path: str = Field(..., description="Path to .sqlite backup")
|
|
801
|
+
context_snapshot_path: str = Field(..., description="Path to context JSON")
|
|
802
|
+
metadata: CheckpointMetadata
|
|
803
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
804
|
+
|
|
805
|
+
def validate_files_exist(self) -> bool:
|
|
806
|
+
"""Check if all checkpoint files exist."""
|
|
807
|
+
from pathlib import Path
|
|
808
|
+
|
|
809
|
+
db_path = Path(self.database_backup_path)
|
|
810
|
+
context_path = Path(self.context_snapshot_path)
|
|
811
|
+
return db_path.exists() and context_path.exists()
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
# =============================================================================
|
|
815
|
+
# Execution Event Models for SSE/WebSocket Streaming
|
|
816
|
+
# =============================================================================
|
|
817
|
+
|
|
818
|
+
ExecutionEventType = Literal[
|
|
819
|
+
"progress", "output", "blocker", "completion", "error", "heartbeat"
|
|
820
|
+
]
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class BaseExecutionEvent(BaseModel):
|
|
824
|
+
"""Base class for all execution events.
|
|
825
|
+
|
|
826
|
+
These events are used for real-time streaming via SSE/WebSocket.
|
|
827
|
+
Each event type has specific data fields in a nested 'data' dict.
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
event_type: ExecutionEventType
|
|
831
|
+
task_id: str
|
|
832
|
+
timestamp: str = Field(
|
|
833
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
model_config = ConfigDict(extra="forbid")
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
class AgentPhase:
|
|
840
|
+
"""Phase constants for ReactAgent progress reporting.
|
|
841
|
+
|
|
842
|
+
Each phase maps to specific tool calls during agent execution.
|
|
843
|
+
"""
|
|
844
|
+
|
|
845
|
+
EXPLORING = "exploring" # read_file, list_files, search_codebase
|
|
846
|
+
PLANNING = "planning" # context loading, system prompt construction
|
|
847
|
+
CREATING = "creating" # create_file
|
|
848
|
+
EDITING = "editing" # edit_file
|
|
849
|
+
TESTING = "testing" # run_tests, run_command
|
|
850
|
+
FIXING = "fixing" # edit_file after verification failure
|
|
851
|
+
VERIFYING = "verifying" # gates.run() / final verification
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class ProgressEvent(BaseExecutionEvent):
|
|
855
|
+
"""Event indicating task execution progress.
|
|
856
|
+
|
|
857
|
+
Emitted when the agent transitions between phases or steps.
|
|
858
|
+
"""
|
|
859
|
+
|
|
860
|
+
event_type: Literal["progress"] = "progress"
|
|
861
|
+
phase: str # planning, execution, verification, self_correction
|
|
862
|
+
step: int
|
|
863
|
+
total_steps: int
|
|
864
|
+
message: Optional[str] = None
|
|
865
|
+
tool_name: Optional[str] = None
|
|
866
|
+
file_path: Optional[str] = None
|
|
867
|
+
iteration: Optional[int] = None
|
|
868
|
+
|
|
869
|
+
@computed_field # type: ignore[prop-decorator]
|
|
870
|
+
@property
|
|
871
|
+
def data(self) -> Dict[str, Any]:
|
|
872
|
+
"""Nested data for SSE format compatibility."""
|
|
873
|
+
return {
|
|
874
|
+
"phase": self.phase,
|
|
875
|
+
"step": self.step,
|
|
876
|
+
"total_steps": self.total_steps,
|
|
877
|
+
"message": self.message,
|
|
878
|
+
"tool_name": self.tool_name,
|
|
879
|
+
"file_path": self.file_path,
|
|
880
|
+
"iteration": self.iteration,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
class OutputEvent(BaseExecutionEvent):
|
|
885
|
+
"""Event containing stdout/stderr output.
|
|
886
|
+
|
|
887
|
+
Emitted when the agent produces command output.
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
event_type: Literal["output"] = "output"
|
|
891
|
+
stream: Literal["stdout", "stderr"]
|
|
892
|
+
line: str
|
|
893
|
+
|
|
894
|
+
@computed_field # type: ignore[prop-decorator]
|
|
895
|
+
@property
|
|
896
|
+
def data(self) -> Dict[str, str]:
|
|
897
|
+
"""Nested data for SSE format compatibility."""
|
|
898
|
+
return {
|
|
899
|
+
"stream": self.stream,
|
|
900
|
+
"line": self.line,
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
class BlockerEvent(BaseExecutionEvent):
|
|
905
|
+
"""Event indicating a blocker was created.
|
|
906
|
+
|
|
907
|
+
Emitted when the agent needs human input to proceed.
|
|
908
|
+
"""
|
|
909
|
+
|
|
910
|
+
event_type: Literal["blocker"] = "blocker"
|
|
911
|
+
blocker_id: int
|
|
912
|
+
question: str
|
|
913
|
+
context: Optional[str] = None
|
|
914
|
+
|
|
915
|
+
@computed_field # type: ignore[prop-decorator]
|
|
916
|
+
@property
|
|
917
|
+
def data(self) -> Dict[str, Any]:
|
|
918
|
+
"""Nested data for SSE format compatibility."""
|
|
919
|
+
return {
|
|
920
|
+
"blocker_id": self.blocker_id,
|
|
921
|
+
"question": self.question,
|
|
922
|
+
"context": self.context,
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class CompletionEvent(BaseExecutionEvent):
|
|
927
|
+
"""Event indicating task execution completed.
|
|
928
|
+
|
|
929
|
+
Emitted when the agent finishes (successfully or with failure).
|
|
930
|
+
"""
|
|
931
|
+
|
|
932
|
+
event_type: Literal["completion"] = "completion"
|
|
933
|
+
status: Literal["completed", "failed", "blocked"]
|
|
934
|
+
duration_seconds: float
|
|
935
|
+
files_modified: Optional[List[str]] = None
|
|
936
|
+
|
|
937
|
+
@computed_field # type: ignore[prop-decorator]
|
|
938
|
+
@property
|
|
939
|
+
def data(self) -> Dict[str, Any]:
|
|
940
|
+
"""Nested data for SSE format compatibility."""
|
|
941
|
+
return {
|
|
942
|
+
"status": self.status,
|
|
943
|
+
"duration_seconds": self.duration_seconds,
|
|
944
|
+
"files_modified": self.files_modified,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class ErrorEvent(BaseExecutionEvent):
|
|
949
|
+
"""Event indicating an error occurred.
|
|
950
|
+
|
|
951
|
+
Emitted when the agent encounters an error during execution.
|
|
952
|
+
"""
|
|
953
|
+
|
|
954
|
+
event_type: Literal["error"] = "error"
|
|
955
|
+
error: str
|
|
956
|
+
error_type: str
|
|
957
|
+
traceback: Optional[str] = None
|
|
958
|
+
|
|
959
|
+
@computed_field # type: ignore[prop-decorator]
|
|
960
|
+
@property
|
|
961
|
+
def data(self) -> Dict[str, Any]:
|
|
962
|
+
"""Nested data for SSE format compatibility."""
|
|
963
|
+
return {
|
|
964
|
+
"error": self.error,
|
|
965
|
+
"error_type": self.error_type,
|
|
966
|
+
"traceback": self.traceback,
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
class HeartbeatEvent(BaseExecutionEvent):
|
|
971
|
+
"""Keep-alive event for long-running streams.
|
|
972
|
+
|
|
973
|
+
Emitted periodically to prevent connection timeouts.
|
|
974
|
+
"""
|
|
975
|
+
|
|
976
|
+
event_type: Literal["heartbeat"] = "heartbeat"
|
|
977
|
+
|
|
978
|
+
@computed_field # type: ignore[prop-decorator]
|
|
979
|
+
@property
|
|
980
|
+
def data(self) -> Dict[str, Any]:
|
|
981
|
+
"""Nested data for SSE format compatibility."""
|
|
982
|
+
return {}
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
# Union type for all execution events
|
|
986
|
+
ExecutionEvent = Union[
|
|
987
|
+
ProgressEvent,
|
|
988
|
+
OutputEvent,
|
|
989
|
+
BlockerEvent,
|
|
990
|
+
CompletionEvent,
|
|
991
|
+
ErrorEvent,
|
|
992
|
+
HeartbeatEvent,
|
|
993
|
+
]
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def create_execution_event(
|
|
997
|
+
event_type: ExecutionEventType,
|
|
998
|
+
task_id: str,
|
|
999
|
+
**kwargs: Any,
|
|
1000
|
+
) -> ExecutionEvent:
|
|
1001
|
+
"""Factory function to create the appropriate event type.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
event_type: Type of event to create
|
|
1005
|
+
task_id: Task ID for the event
|
|
1006
|
+
**kwargs: Event-specific fields
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
The appropriate ExecutionEvent subclass instance
|
|
1010
|
+
|
|
1011
|
+
Raises:
|
|
1012
|
+
ValueError: If event_type is not recognized
|
|
1013
|
+
"""
|
|
1014
|
+
event_classes: Dict[str, type] = {
|
|
1015
|
+
"progress": ProgressEvent,
|
|
1016
|
+
"output": OutputEvent,
|
|
1017
|
+
"blocker": BlockerEvent,
|
|
1018
|
+
"completion": CompletionEvent,
|
|
1019
|
+
"error": ErrorEvent,
|
|
1020
|
+
"heartbeat": HeartbeatEvent,
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if event_type not in event_classes:
|
|
1024
|
+
raise ValueError(f"Unknown event type: {event_type}")
|
|
1025
|
+
|
|
1026
|
+
return event_classes[event_type](task_id=task_id, **kwargs)
|