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,542 @@
|
|
|
1
|
+
"""Task context loader for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
Builds context for agent execution by loading task, PRD, and codebase information.
|
|
4
|
+
Manages context window size to stay within model limits.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import fnmatch
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from codeframe.core.workspace import Workspace
|
|
17
|
+
from codeframe.core import tasks, prd, blockers
|
|
18
|
+
from codeframe.core.tasks import Task
|
|
19
|
+
from codeframe.core.prd import PrdRecord
|
|
20
|
+
from codeframe.core.blockers import Blocker, BlockerStatus
|
|
21
|
+
from codeframe.core.agents_config import (
|
|
22
|
+
AgentPreferences,
|
|
23
|
+
load_preferences,
|
|
24
|
+
get_default_preferences,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Approximate tokens per character (conservative estimate)
|
|
29
|
+
CHARS_PER_TOKEN = 4
|
|
30
|
+
|
|
31
|
+
# Default context limits
|
|
32
|
+
DEFAULT_MAX_TOKENS = 100_000
|
|
33
|
+
DEFAULT_FILE_TOKENS = 2_000
|
|
34
|
+
|
|
35
|
+
# Files to always ignore
|
|
36
|
+
DEFAULT_IGNORE_PATTERNS = [
|
|
37
|
+
".git/*",
|
|
38
|
+
".git",
|
|
39
|
+
"__pycache__/*",
|
|
40
|
+
"*.pyc",
|
|
41
|
+
".venv/*",
|
|
42
|
+
"venv/*",
|
|
43
|
+
"node_modules/*",
|
|
44
|
+
".next/*",
|
|
45
|
+
"dist/*",
|
|
46
|
+
"build/*",
|
|
47
|
+
"*.egg-info/*",
|
|
48
|
+
".pytest_cache/*",
|
|
49
|
+
".mypy_cache/*",
|
|
50
|
+
".ruff_cache/*",
|
|
51
|
+
"*.lock",
|
|
52
|
+
"package-lock.json",
|
|
53
|
+
"*.min.js",
|
|
54
|
+
"*.min.css",
|
|
55
|
+
".codeframe/*",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# File extensions to include by default
|
|
59
|
+
CODE_EXTENSIONS = {
|
|
60
|
+
".py", ".js", ".ts", ".tsx", ".jsx",
|
|
61
|
+
".go", ".rs", ".java", ".kt",
|
|
62
|
+
".c", ".cpp", ".h", ".hpp",
|
|
63
|
+
".rb", ".php", ".swift",
|
|
64
|
+
".md", ".txt", ".json", ".yaml", ".yml", ".toml",
|
|
65
|
+
".html", ".css", ".scss", ".sql",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class FileInfo:
|
|
71
|
+
"""Information about a source file.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
path: Relative path from repo root
|
|
75
|
+
size_bytes: File size in bytes
|
|
76
|
+
extension: File extension
|
|
77
|
+
relevance_score: How relevant to the task (0.0 - 1.0)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
path: str
|
|
81
|
+
size_bytes: int
|
|
82
|
+
extension: str
|
|
83
|
+
relevance_score: float = 0.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class FileContent:
|
|
88
|
+
"""A file with its content loaded.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
path: Relative path from repo root
|
|
92
|
+
content: File content
|
|
93
|
+
tokens_estimate: Estimated token count
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
path: str
|
|
97
|
+
content: str
|
|
98
|
+
tokens_estimate: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class TaskContext:
|
|
103
|
+
"""Complete context for executing a task.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
task: The task to execute
|
|
107
|
+
prd: Associated PRD (if any)
|
|
108
|
+
blockers: Blockers related to this task
|
|
109
|
+
preferences: Agent preferences from AGENTS.md/CLAUDE.md
|
|
110
|
+
tech_stack: Natural language description of project's technology stack
|
|
111
|
+
file_tree: List of files in the repository
|
|
112
|
+
relevant_files: Files identified as relevant to the task
|
|
113
|
+
loaded_files: Files with content loaded
|
|
114
|
+
total_tokens: Estimated total token count
|
|
115
|
+
max_tokens: Maximum allowed tokens
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
task: Task
|
|
119
|
+
prd: Optional[PrdRecord] = None
|
|
120
|
+
blockers: list[Blocker] = field(default_factory=list)
|
|
121
|
+
preferences: AgentPreferences = field(default_factory=get_default_preferences)
|
|
122
|
+
tech_stack: Optional[str] = None
|
|
123
|
+
file_tree: list[FileInfo] = field(default_factory=list)
|
|
124
|
+
relevant_files: list[FileInfo] = field(default_factory=list)
|
|
125
|
+
loaded_files: list[FileContent] = field(default_factory=list)
|
|
126
|
+
total_tokens: int = 0
|
|
127
|
+
max_tokens: int = DEFAULT_MAX_TOKENS
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def has_prd(self) -> bool:
|
|
131
|
+
"""Check if PRD is loaded."""
|
|
132
|
+
return self.prd is not None
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def has_blockers(self) -> bool:
|
|
136
|
+
"""Check if there are any blockers."""
|
|
137
|
+
return len(self.blockers) > 0
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def has_tech_stack(self) -> bool:
|
|
141
|
+
"""Check if tech stack is defined."""
|
|
142
|
+
return bool(self.tech_stack)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def open_blockers(self) -> list[Blocker]:
|
|
146
|
+
"""Get open blockers only."""
|
|
147
|
+
return [b for b in self.blockers if b.status == BlockerStatus.OPEN]
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def answered_blockers(self) -> list[Blocker]:
|
|
151
|
+
"""Get answered blockers (useful context)."""
|
|
152
|
+
return [b for b in self.blockers if b.status == BlockerStatus.ANSWERED]
|
|
153
|
+
|
|
154
|
+
def tokens_remaining(self) -> int:
|
|
155
|
+
"""Calculate remaining token budget."""
|
|
156
|
+
return max(0, self.max_tokens - self.total_tokens)
|
|
157
|
+
|
|
158
|
+
def to_prompt_context(self) -> str:
|
|
159
|
+
"""Convert to a formatted string for LLM prompts.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Formatted context string
|
|
163
|
+
"""
|
|
164
|
+
sections = []
|
|
165
|
+
|
|
166
|
+
# Task section
|
|
167
|
+
sections.append("## Task")
|
|
168
|
+
sections.append(f"**Title:** {self.task.title}")
|
|
169
|
+
if self.task.description:
|
|
170
|
+
sections.append(f"**Description:** {self.task.description}")
|
|
171
|
+
sections.append(f"**Status:** {self.task.status.value}")
|
|
172
|
+
sections.append("")
|
|
173
|
+
|
|
174
|
+
# PRD section
|
|
175
|
+
if self.prd:
|
|
176
|
+
sections.append("## Requirements (PRD)")
|
|
177
|
+
sections.append(f"**Title:** {self.prd.title}")
|
|
178
|
+
sections.append("")
|
|
179
|
+
sections.append(self.prd.content[:10000]) # Limit PRD content
|
|
180
|
+
sections.append("")
|
|
181
|
+
|
|
182
|
+
# Project preferences section (from AGENTS.md/CLAUDE.md)
|
|
183
|
+
if self.preferences and self.preferences.has_preferences():
|
|
184
|
+
pref_section = self.preferences.to_prompt_section()
|
|
185
|
+
if pref_section:
|
|
186
|
+
sections.append(pref_section)
|
|
187
|
+
sections.append("")
|
|
188
|
+
|
|
189
|
+
# Tech stack section
|
|
190
|
+
if self.tech_stack:
|
|
191
|
+
sections.append("## Project Tech Stack")
|
|
192
|
+
sections.append(f"**Technology:** {self.tech_stack}")
|
|
193
|
+
sections.append("")
|
|
194
|
+
sections.append("Use appropriate commands and patterns for this technology stack.")
|
|
195
|
+
sections.append("")
|
|
196
|
+
|
|
197
|
+
# Blockers section (answered ones are useful context)
|
|
198
|
+
if self.answered_blockers:
|
|
199
|
+
sections.append("## Previous Clarifications")
|
|
200
|
+
for b in self.answered_blockers:
|
|
201
|
+
sections.append(f"**Q:** {b.question}")
|
|
202
|
+
sections.append(f"**A:** {b.answer}")
|
|
203
|
+
sections.append("")
|
|
204
|
+
|
|
205
|
+
# File tree summary
|
|
206
|
+
if self.file_tree:
|
|
207
|
+
sections.append("## Repository Structure")
|
|
208
|
+
sections.append(f"Total files: {len(self.file_tree)}")
|
|
209
|
+
# Group by directory
|
|
210
|
+
dirs = {}
|
|
211
|
+
for f in self.file_tree[:100]: # Limit to first 100
|
|
212
|
+
dir_path = str(Path(f.path).parent)
|
|
213
|
+
if dir_path not in dirs:
|
|
214
|
+
dirs[dir_path] = []
|
|
215
|
+
dirs[dir_path].append(f.path)
|
|
216
|
+
for dir_path in sorted(dirs.keys())[:20]: # Limit directories
|
|
217
|
+
sections.append(f" {dir_path}/")
|
|
218
|
+
for file_path in dirs[dir_path][:5]: # Limit files per dir
|
|
219
|
+
sections.append(f" {Path(file_path).name}")
|
|
220
|
+
if len(dirs[dir_path]) > 5:
|
|
221
|
+
sections.append(f" ... and {len(dirs[dir_path]) - 5} more")
|
|
222
|
+
sections.append("")
|
|
223
|
+
|
|
224
|
+
# Loaded file contents
|
|
225
|
+
if self.loaded_files:
|
|
226
|
+
sections.append("## Relevant Source Files")
|
|
227
|
+
for f in self.loaded_files:
|
|
228
|
+
sections.append(f"### {f.path}")
|
|
229
|
+
sections.append("```")
|
|
230
|
+
sections.append(f.content)
|
|
231
|
+
sections.append("```")
|
|
232
|
+
sections.append("")
|
|
233
|
+
|
|
234
|
+
return "\n".join(sections)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ContextLoader:
|
|
238
|
+
"""Loads and builds context for task execution.
|
|
239
|
+
|
|
240
|
+
Handles loading task metadata, PRD content, codebase structure,
|
|
241
|
+
and relevant file contents while respecting token limits.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
workspace: Workspace,
|
|
247
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
248
|
+
ignore_patterns: Optional[list[str]] = None,
|
|
249
|
+
):
|
|
250
|
+
"""Initialize the context loader.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
workspace: Target workspace
|
|
254
|
+
max_tokens: Maximum tokens for context
|
|
255
|
+
ignore_patterns: File patterns to ignore
|
|
256
|
+
"""
|
|
257
|
+
self.workspace = workspace
|
|
258
|
+
self.max_tokens = max_tokens
|
|
259
|
+
self.ignore_patterns = ignore_patterns or DEFAULT_IGNORE_PATTERNS
|
|
260
|
+
|
|
261
|
+
def load(self, task_id: str) -> TaskContext:
|
|
262
|
+
"""Load complete context for a task.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
task_id: Task to load context for
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
TaskContext with all loaded information
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ValueError: If task not found
|
|
272
|
+
"""
|
|
273
|
+
# Load task
|
|
274
|
+
task = tasks.get(self.workspace, task_id)
|
|
275
|
+
if not task:
|
|
276
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
277
|
+
|
|
278
|
+
context = TaskContext(task=task, max_tokens=self.max_tokens)
|
|
279
|
+
|
|
280
|
+
# Load agent preferences from AGENTS.md/CLAUDE.md
|
|
281
|
+
context.preferences = load_preferences(self.workspace.repo_path)
|
|
282
|
+
|
|
283
|
+
# Load tech stack from workspace
|
|
284
|
+
context.tech_stack = self.workspace.tech_stack
|
|
285
|
+
|
|
286
|
+
# Load PRD if associated
|
|
287
|
+
if task.prd_id:
|
|
288
|
+
context.prd = prd.get_by_id(self.workspace, task.prd_id)
|
|
289
|
+
|
|
290
|
+
# Load blockers for this task
|
|
291
|
+
context.blockers = blockers.list_for_task(self.workspace, task_id)
|
|
292
|
+
|
|
293
|
+
# Build file tree
|
|
294
|
+
context.file_tree = self._scan_file_tree()
|
|
295
|
+
|
|
296
|
+
# Score and select relevant files
|
|
297
|
+
context.relevant_files = self._score_relevance(
|
|
298
|
+
context.file_tree, task, context.prd
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Load file contents within token budget
|
|
302
|
+
self._load_file_contents(context)
|
|
303
|
+
|
|
304
|
+
return context
|
|
305
|
+
|
|
306
|
+
def _scan_file_tree(self) -> list[FileInfo]:
|
|
307
|
+
"""Scan repository for source files.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
List of FileInfo for all relevant files
|
|
311
|
+
"""
|
|
312
|
+
files = []
|
|
313
|
+
repo_path = self.workspace.repo_path
|
|
314
|
+
|
|
315
|
+
for root, dirs, filenames in os.walk(repo_path):
|
|
316
|
+
# Filter directories
|
|
317
|
+
dirs[:] = [
|
|
318
|
+
d for d in dirs
|
|
319
|
+
if not self._should_ignore(os.path.join(root, d), repo_path)
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
for filename in filenames:
|
|
323
|
+
file_path = os.path.join(root, filename)
|
|
324
|
+
rel_path = os.path.relpath(file_path, repo_path)
|
|
325
|
+
|
|
326
|
+
if self._should_ignore(file_path, repo_path):
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
330
|
+
if ext not in CODE_EXTENSIONS:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
size = os.path.getsize(file_path)
|
|
335
|
+
files.append(FileInfo(
|
|
336
|
+
path=rel_path,
|
|
337
|
+
size_bytes=size,
|
|
338
|
+
extension=ext,
|
|
339
|
+
))
|
|
340
|
+
except OSError:
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
return files
|
|
344
|
+
|
|
345
|
+
def _should_ignore(self, path: str, repo_path: Path) -> bool:
|
|
346
|
+
"""Check if a path should be ignored.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
path: Absolute path to check
|
|
350
|
+
repo_path: Repository root path
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if path should be ignored
|
|
354
|
+
"""
|
|
355
|
+
rel_path = os.path.relpath(path, repo_path)
|
|
356
|
+
|
|
357
|
+
for pattern in self.ignore_patterns:
|
|
358
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
359
|
+
return True
|
|
360
|
+
if fnmatch.fnmatch(os.path.basename(path), pattern):
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def _score_relevance(
|
|
366
|
+
self,
|
|
367
|
+
files: list[FileInfo],
|
|
368
|
+
task: Task,
|
|
369
|
+
prd_record: Optional[PrdRecord],
|
|
370
|
+
) -> list[FileInfo]:
|
|
371
|
+
"""Score files by relevance to the task.
|
|
372
|
+
|
|
373
|
+
Uses keyword matching from task title/description and PRD.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
files: Files to score
|
|
377
|
+
task: Target task
|
|
378
|
+
prd_record: Associated PRD (if any)
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Files sorted by relevance score (highest first)
|
|
382
|
+
"""
|
|
383
|
+
# Extract keywords from task and PRD
|
|
384
|
+
keywords = self._extract_keywords(task.title + " " + task.description)
|
|
385
|
+
if prd_record:
|
|
386
|
+
keywords.update(self._extract_keywords(prd_record.content[:5000]))
|
|
387
|
+
|
|
388
|
+
scored_files = []
|
|
389
|
+
for file_info in files:
|
|
390
|
+
score = self._calculate_relevance(file_info, keywords)
|
|
391
|
+
file_info.relevance_score = score
|
|
392
|
+
scored_files.append(file_info)
|
|
393
|
+
|
|
394
|
+
# Sort by relevance (highest first)
|
|
395
|
+
scored_files.sort(key=lambda f: f.relevance_score, reverse=True)
|
|
396
|
+
|
|
397
|
+
return scored_files
|
|
398
|
+
|
|
399
|
+
def _extract_keywords(self, text: str) -> set[str]:
|
|
400
|
+
"""Extract meaningful keywords from text.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
text: Text to extract keywords from
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
Set of lowercase keywords
|
|
407
|
+
"""
|
|
408
|
+
# Remove markdown formatting
|
|
409
|
+
text = re.sub(r"[#*`\[\]()]", " ", text)
|
|
410
|
+
|
|
411
|
+
# Split into words
|
|
412
|
+
words = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", text.lower())
|
|
413
|
+
|
|
414
|
+
# Filter out common words
|
|
415
|
+
stopwords = {
|
|
416
|
+
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to",
|
|
417
|
+
"for", "of", "with", "by", "from", "is", "are", "was", "were",
|
|
418
|
+
"be", "been", "being", "have", "has", "had", "do", "does", "did",
|
|
419
|
+
"will", "would", "could", "should", "may", "might", "must",
|
|
420
|
+
"this", "that", "these", "those", "it", "its",
|
|
421
|
+
"as", "if", "then", "else", "when", "where", "which", "who",
|
|
422
|
+
"what", "how", "why", "all", "each", "every", "both", "few",
|
|
423
|
+
"more", "most", "other", "some", "such", "no", "not", "only",
|
|
424
|
+
"own", "same", "so", "than", "too", "very", "just", "can",
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
keywords = {w for w in words if len(w) > 2 and w not in stopwords}
|
|
428
|
+
return keywords
|
|
429
|
+
|
|
430
|
+
def _calculate_relevance(
|
|
431
|
+
self, file_info: FileInfo, keywords: set[str]
|
|
432
|
+
) -> float:
|
|
433
|
+
"""Calculate relevance score for a file.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
file_info: File to score
|
|
437
|
+
keywords: Keywords to match against
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Relevance score (0.0 - 1.0)
|
|
441
|
+
"""
|
|
442
|
+
score = 0.0
|
|
443
|
+
path_lower = file_info.path.lower()
|
|
444
|
+
path_parts = set(re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", path_lower))
|
|
445
|
+
|
|
446
|
+
# Match keywords in path
|
|
447
|
+
matches = path_parts.intersection(keywords)
|
|
448
|
+
if matches:
|
|
449
|
+
score += len(matches) * 0.2
|
|
450
|
+
|
|
451
|
+
# Boost for certain file types
|
|
452
|
+
if file_info.extension == ".py":
|
|
453
|
+
score += 0.1
|
|
454
|
+
elif file_info.extension in {".ts", ".tsx", ".js", ".jsx"}:
|
|
455
|
+
score += 0.08
|
|
456
|
+
elif file_info.extension == ".md":
|
|
457
|
+
score += 0.05
|
|
458
|
+
|
|
459
|
+
# Boost for key filenames
|
|
460
|
+
filename = os.path.basename(file_info.path).lower()
|
|
461
|
+
if filename in {"readme.md", "readme.txt"}:
|
|
462
|
+
score += 0.15
|
|
463
|
+
elif filename in {"main.py", "app.py", "index.ts", "index.js"}:
|
|
464
|
+
score += 0.12
|
|
465
|
+
elif "test" in filename:
|
|
466
|
+
score += 0.05
|
|
467
|
+
|
|
468
|
+
# Penalize very large files
|
|
469
|
+
if file_info.size_bytes > 50000:
|
|
470
|
+
score *= 0.5
|
|
471
|
+
elif file_info.size_bytes > 20000:
|
|
472
|
+
score *= 0.8
|
|
473
|
+
|
|
474
|
+
return min(score, 1.0)
|
|
475
|
+
|
|
476
|
+
def _load_file_contents(self, context: TaskContext) -> None:
|
|
477
|
+
"""Load file contents within token budget.
|
|
478
|
+
|
|
479
|
+
Modifies context in place to add loaded_files and update total_tokens.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
context: Context to update
|
|
483
|
+
"""
|
|
484
|
+
# Reserve tokens for task metadata and PRD
|
|
485
|
+
reserved_tokens = 0
|
|
486
|
+
if context.task:
|
|
487
|
+
reserved_tokens += self._estimate_tokens(
|
|
488
|
+
context.task.title + context.task.description
|
|
489
|
+
)
|
|
490
|
+
if context.prd:
|
|
491
|
+
reserved_tokens += self._estimate_tokens(context.prd.content[:10000])
|
|
492
|
+
|
|
493
|
+
context.total_tokens = reserved_tokens
|
|
494
|
+
|
|
495
|
+
# Load files by relevance until budget exhausted
|
|
496
|
+
for file_info in context.relevant_files:
|
|
497
|
+
if context.tokens_remaining() < DEFAULT_FILE_TOKENS:
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
file_content = self._load_file(file_info)
|
|
501
|
+
if file_content:
|
|
502
|
+
if file_content.tokens_estimate <= context.tokens_remaining():
|
|
503
|
+
context.loaded_files.append(file_content)
|
|
504
|
+
context.total_tokens += file_content.tokens_estimate
|
|
505
|
+
|
|
506
|
+
def _load_file(self, file_info: FileInfo) -> Optional[FileContent]:
|
|
507
|
+
"""Load content of a single file.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
file_info: File to load
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
FileContent or None if file can't be read
|
|
514
|
+
"""
|
|
515
|
+
file_path = self.workspace.repo_path / file_info.path
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
519
|
+
|
|
520
|
+
# Truncate very large files
|
|
521
|
+
max_chars = DEFAULT_FILE_TOKENS * CHARS_PER_TOKEN
|
|
522
|
+
if len(content) > max_chars:
|
|
523
|
+
content = content[:max_chars] + "\n... (truncated)"
|
|
524
|
+
|
|
525
|
+
return FileContent(
|
|
526
|
+
path=file_info.path,
|
|
527
|
+
content=content,
|
|
528
|
+
tokens_estimate=self._estimate_tokens(content),
|
|
529
|
+
)
|
|
530
|
+
except Exception:
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
def _estimate_tokens(self, text: str) -> int:
|
|
534
|
+
"""Estimate token count for text.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
text: Text to estimate
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
Estimated token count
|
|
541
|
+
"""
|
|
542
|
+
return len(text) // CHARS_PER_TOKEN
|