codeframe-ai 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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