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,827 @@
1
+ """Configuration management for CodeFRAME.
2
+
3
+ This module provides two configuration systems:
4
+ 1. Legacy v1 config (JSON-based): ProjectConfig, GlobalConfig
5
+ 2. v2 environment config (YAML-based): EnvironmentConfig
6
+
7
+ v2 environment config is stored in .codeframe/config.yaml and controls:
8
+ - Package manager (uv, pip, poetry, npm, etc.)
9
+ - Language versions (Python, Node)
10
+ - Test framework (pytest, jest, vitest)
11
+ - Lint tools (ruff, eslint, prettier)
12
+ """
13
+
14
+ import json
15
+ from dataclasses import dataclass, field as dataclass_field, asdict
16
+ from enum import Enum
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ import yaml
21
+ from pydantic import BaseModel, Field, field_validator
22
+ from pydantic_settings import BaseSettings, SettingsConfigDict
23
+ from dotenv import load_dotenv
24
+
25
+
26
+ # =============================================================================
27
+ # v2 Environment Configuration (YAML-based)
28
+ # =============================================================================
29
+
30
+
31
+ class PackageManager(str, Enum):
32
+ """Supported package managers."""
33
+
34
+ UV = "uv"
35
+ PIP = "pip"
36
+ POETRY = "poetry"
37
+ NPM = "npm"
38
+ PNPM = "pnpm"
39
+ YARN = "yarn"
40
+
41
+
42
+ class TestFramework(str, Enum):
43
+ """Supported test frameworks."""
44
+
45
+ PYTEST = "pytest"
46
+ JEST = "jest"
47
+ VITEST = "vitest"
48
+ UNITTEST = "unittest"
49
+ MOCHA = "mocha"
50
+
51
+
52
+ class LintTool(str, Enum):
53
+ """Supported lint tools."""
54
+
55
+ RUFF = "ruff"
56
+ PYLINT = "pylint"
57
+ FLAKE8 = "flake8"
58
+ MYPY = "mypy"
59
+ ESLINT = "eslint"
60
+ PRETTIER = "prettier"
61
+ BIOME = "biome"
62
+
63
+
64
+ @dataclass
65
+ class LLMConfig:
66
+ """Per-workspace LLM provider configuration.
67
+
68
+ Stored under the ``llm:`` key in ``.codeframe/config.yaml``.
69
+ Priority (lowest → highest): config file → env var → CLI flag.
70
+ """
71
+
72
+ provider: Optional[str] = None # e.g. "anthropic", "openai", "ollama"
73
+ model: Optional[str] = None # e.g. "gpt-4o", "qwen2.5-coder:7b"
74
+ base_url: Optional[str] = None # e.g. "http://localhost:11434/v1"
75
+
76
+
77
+ @dataclass
78
+ class ContextConfig:
79
+ """Context loading configuration."""
80
+
81
+ max_files: int = 20
82
+ max_file_size: int = 5000 # lines
83
+ max_total_tokens: int = 50000
84
+
85
+
86
+ @dataclass
87
+ class AgentBudgetConfig:
88
+ """Agent iteration budget configuration."""
89
+ base_iterations: int = 30
90
+ min_iterations: int = 15
91
+ max_iterations: int = 100
92
+ auto_fix_enabled: bool = True
93
+ early_termination_enabled: bool = True
94
+ stall_timeout_s: int = 300
95
+
96
+
97
+ @dataclass
98
+ class BatchConfig:
99
+ """Batch execution configuration."""
100
+
101
+ max_parallel: int = 4
102
+ max_parallel_by_status: dict[str, int] = dataclass_field(default_factory=dict)
103
+
104
+
105
+ @dataclass
106
+ class HooksConfig:
107
+ """Workspace lifecycle hooks configuration.
108
+
109
+ Hooks are shell commands executed at specific lifecycle points.
110
+ Template variables (e.g., {{task_id}}) are rendered via Jinja2.
111
+ """
112
+
113
+ after_init: Optional[str] = None
114
+ before_task: Optional[str] = None
115
+ after_task_success: Optional[str] = None
116
+ after_task_failure: Optional[str] = None
117
+ before_remove: Optional[str] = None
118
+ hook_timeout: int = 60
119
+
120
+
121
+ @dataclass
122
+ class EnvironmentConfig:
123
+ """v2 project environment configuration.
124
+
125
+ Stored in .codeframe/config.yaml. Controls how the agent
126
+ interacts with the project's development environment.
127
+ """
128
+
129
+ # Package management
130
+ package_manager: str = "uv"
131
+ python_version: Optional[str] = None # e.g., "3.11"
132
+ node_version: Optional[str] = None # e.g., "18"
133
+
134
+ # Testing
135
+ test_framework: str = "pytest"
136
+ test_command: Optional[str] = None # Override, e.g., "pytest -v tests/"
137
+
138
+ # Linting
139
+ lint_tools: list[str] = dataclass_field(default_factory=lambda: ["ruff"])
140
+ lint_command: Optional[str] = None # Override, e.g., "ruff check ."
141
+
142
+ # Context loading
143
+ context: ContextConfig = dataclass_field(default_factory=ContextConfig)
144
+
145
+ # Agent budget
146
+ agent_budget: AgentBudgetConfig = dataclass_field(default_factory=AgentBudgetConfig)
147
+
148
+ # Batch execution
149
+ batch: BatchConfig = dataclass_field(default_factory=BatchConfig)
150
+
151
+ # Workspace lifecycle hooks
152
+ hooks: HooksConfig = dataclass_field(default_factory=HooksConfig)
153
+
154
+ # Reconciliation during batch execution
155
+ reconciliation_interval_seconds: int = 30
156
+
157
+ # Execution engine
158
+ engine: str = "react"
159
+
160
+ # Custom command overrides
161
+ custom_commands: dict[str, str] = dataclass_field(default_factory=dict)
162
+
163
+ # LLM provider config (workspace-level default; overridden by env vars and CLI flags)
164
+ llm: Optional[LLMConfig] = None
165
+
166
+ # Settings page (issue #554) — UI-managed agent settings
167
+ max_cost_usd: Optional[float] = None
168
+ agent_type_models: dict[str, str] = dataclass_field(default_factory=dict)
169
+
170
+ def validate(self) -> list[str]:
171
+ """Validate configuration values.
172
+
173
+ Returns:
174
+ List of validation error messages (empty if valid)
175
+ """
176
+ errors = []
177
+
178
+ # Validate package manager
179
+ valid_pkg_managers = [pm.value for pm in PackageManager]
180
+ if self.package_manager not in valid_pkg_managers:
181
+ errors.append(
182
+ f"Invalid package_manager '{self.package_manager}'. "
183
+ f"Must be one of: {', '.join(valid_pkg_managers)}"
184
+ )
185
+
186
+ # Validate test framework
187
+ valid_test_frameworks = [tf.value for tf in TestFramework]
188
+ if self.test_framework not in valid_test_frameworks:
189
+ errors.append(
190
+ f"Invalid test_framework '{self.test_framework}'. "
191
+ f"Must be one of: {', '.join(valid_test_frameworks)}"
192
+ )
193
+
194
+ # Validate lint tools
195
+ valid_lint_tools = [lt.value for lt in LintTool]
196
+ for tool in self.lint_tools:
197
+ if tool not in valid_lint_tools:
198
+ errors.append(
199
+ f"Invalid lint tool '{tool}'. "
200
+ f"Must be one of: {', '.join(valid_lint_tools)}"
201
+ )
202
+
203
+ # Validate engine
204
+ from codeframe.core.engine_registry import VALID_ENGINES
205
+ if self.engine not in VALID_ENGINES:
206
+ errors.append(
207
+ f"Invalid engine '{self.engine}'. "
208
+ f"Must be one of: {', '.join(sorted(VALID_ENGINES))}"
209
+ )
210
+
211
+ # Validate agent budget
212
+ budget = self.agent_budget
213
+ if any(v <= 0 for v in (budget.base_iterations, budget.min_iterations, budget.max_iterations)):
214
+ errors.append("agent_budget iterations must be positive integers")
215
+ if budget.min_iterations > budget.max_iterations:
216
+ errors.append("agent_budget.min_iterations cannot exceed max_iterations")
217
+ if not (budget.min_iterations <= budget.base_iterations <= budget.max_iterations):
218
+ errors.append(
219
+ "agent_budget.base_iterations must be between min_iterations and max_iterations"
220
+ )
221
+ if budget.stall_timeout_s < 0:
222
+ errors.append("agent_budget.stall_timeout_s must be >= 0 (0 = disabled)")
223
+
224
+ if self.max_cost_usd is not None and self.max_cost_usd < 0:
225
+ errors.append("max_cost_usd must be >= 0")
226
+
227
+ valid_agent_types = {"claude_code", "codex", "opencode", "react"}
228
+ invalid_agent_types = sorted(set(self.agent_type_models) - valid_agent_types)
229
+ if invalid_agent_types:
230
+ errors.append(
231
+ "agent_type_models contains unsupported agent types: "
232
+ + ", ".join(invalid_agent_types)
233
+ )
234
+
235
+ return errors
236
+
237
+ def get_install_command(self, package: str) -> str:
238
+ """Get the install command for a package.
239
+
240
+ Args:
241
+ package: Package name to install
242
+
243
+ Returns:
244
+ Full install command string
245
+ """
246
+ if self.package_manager == "uv":
247
+ return f"uv pip install {package}"
248
+ elif self.package_manager == "pip":
249
+ return f"pip install {package}"
250
+ elif self.package_manager == "poetry":
251
+ return f"poetry add {package}"
252
+ elif self.package_manager in ("npm", "pnpm", "yarn"):
253
+ return f"{self.package_manager} install {package}"
254
+ else:
255
+ return f"pip install {package}" # fallback
256
+
257
+ def get_test_command(self) -> str:
258
+ """Get the test command for this project.
259
+
260
+ Returns:
261
+ Test command string
262
+ """
263
+ if self.test_command:
264
+ return self.test_command
265
+
266
+ if self.test_framework == "pytest":
267
+ return "pytest"
268
+ elif self.test_framework == "jest":
269
+ return "npm test" if self.package_manager == "npm" else "jest"
270
+ elif self.test_framework == "vitest":
271
+ return "npm test" if self.package_manager == "npm" else "vitest"
272
+ elif self.test_framework == "unittest":
273
+ return "python -m unittest discover"
274
+ elif self.test_framework == "mocha":
275
+ return "npm test" if self.package_manager == "npm" else "mocha"
276
+ else:
277
+ return "pytest" # fallback
278
+
279
+ def get_lint_command(self) -> str:
280
+ """Get the lint command for this project.
281
+
282
+ Returns:
283
+ Lint command string
284
+ """
285
+ if self.lint_command:
286
+ return self.lint_command
287
+
288
+ if not self.lint_tools:
289
+ return "ruff check ." # default
290
+
291
+ # Use the first lint tool
292
+ tool = self.lint_tools[0]
293
+ if tool == "ruff":
294
+ return "ruff check ."
295
+ elif tool == "pylint":
296
+ return "pylint ."
297
+ elif tool == "flake8":
298
+ return "flake8 ."
299
+ elif tool == "mypy":
300
+ return "mypy ."
301
+ elif tool == "eslint":
302
+ return "eslint ."
303
+ elif tool == "prettier":
304
+ return "prettier --check ."
305
+ elif tool == "biome":
306
+ return "biome check ."
307
+ else:
308
+ return "ruff check ." # fallback
309
+
310
+ def to_dict(self) -> dict[str, Any]:
311
+ """Convert to dictionary for YAML serialization."""
312
+ data = asdict(self)
313
+ # Convert ContextConfig to dict
314
+ if isinstance(data.get("context"), dict):
315
+ pass # already a dict from asdict
316
+ return data
317
+
318
+ @classmethod
319
+ def from_dict(cls, data: dict[str, Any]) -> "EnvironmentConfig":
320
+ """Create from dictionary (YAML deserialization)."""
321
+ # Handle nested ContextConfig
322
+ if "context" in data and isinstance(data["context"], dict):
323
+ data["context"] = ContextConfig(**data["context"])
324
+ if "agent_budget" in data and isinstance(data["agent_budget"], dict):
325
+ data["agent_budget"] = AgentBudgetConfig(**data["agent_budget"])
326
+ if "batch" in data and isinstance(data["batch"], dict):
327
+ data["batch"] = BatchConfig(**data["batch"])
328
+ if "hooks" in data and isinstance(data["hooks"], dict):
329
+ data["hooks"] = HooksConfig(**data["hooks"])
330
+ if "llm" in data and isinstance(data["llm"], dict):
331
+ data["llm"] = LLMConfig(**data["llm"])
332
+ return cls(**data)
333
+
334
+
335
+ # Environment config file name
336
+ ENV_CONFIG_FILE = "config.yaml"
337
+
338
+
339
+ def load_environment_config(workspace_path: Path) -> Optional[EnvironmentConfig]:
340
+ """Load environment configuration from workspace.
341
+
342
+ Checks .codeframe/config.yaml first. If not found, falls back to
343
+ reading CODEFRAME.md front matter and converting it to EnvironmentConfig.
344
+
345
+ Args:
346
+ workspace_path: Path to the workspace root
347
+
348
+ Returns:
349
+ EnvironmentConfig if configuration is found, None otherwise
350
+ """
351
+ # Try .codeframe/config.yaml first (existing behavior)
352
+ config_file = workspace_path / ".codeframe" / ENV_CONFIG_FILE
353
+ if config_file.exists():
354
+ with open(config_file) as f:
355
+ data = yaml.safe_load(f)
356
+
357
+ if data is None:
358
+ return EnvironmentConfig() # empty file = defaults
359
+
360
+ return EnvironmentConfig.from_dict(data)
361
+
362
+ # Fallback: try CODEFRAME.md front matter
363
+ from codeframe.core.agents_config import get_codeframe_config
364
+
365
+ cf_config = get_codeframe_config(workspace_path)
366
+ if cf_config is not None and _has_meaningful_config(cf_config):
367
+ return _codeframe_config_to_env_config(cf_config)
368
+
369
+ return None
370
+
371
+
372
+ def _has_meaningful_config(cf_config: Any) -> bool:
373
+ """Check if a CodeframeConfig has any non-default settings.
374
+
375
+ Returns False when the CODEFRAME.md exists but has no YAML front matter
376
+ (all fields are None or empty).
377
+ """
378
+ return bool(
379
+ cf_config.engine
380
+ or cf_config.tech_stack
381
+ or cf_config.gates
382
+ or cf_config.hooks
383
+ or cf_config.batch
384
+ or cf_config.agent
385
+ )
386
+
387
+
388
+ def _codeframe_config_to_env_config(cf_config: Any) -> EnvironmentConfig:
389
+ """Convert CodeframeConfig (from CODEFRAME.md) to EnvironmentConfig.
390
+
391
+ Maps structured fields from CODEFRAME.md front matter into the
392
+ EnvironmentConfig dataclass used throughout the runtime.
393
+
394
+ Args:
395
+ cf_config: A CodeframeConfig instance from agents_config module.
396
+
397
+ Returns:
398
+ Populated EnvironmentConfig.
399
+ """
400
+ config = EnvironmentConfig()
401
+
402
+ # Map engine directly
403
+ if cf_config.engine:
404
+ config.engine = cf_config.engine
405
+
406
+ # Map tech_stack to package_manager/test_framework heuristics
407
+ if cf_config.tech_stack:
408
+ ts = cf_config.tech_stack.lower()
409
+ if "uv" in ts:
410
+ config.package_manager = "uv"
411
+ elif "poetry" in ts:
412
+ config.package_manager = "poetry"
413
+ elif "npm" in ts:
414
+ config.package_manager = "npm"
415
+ elif "pnpm" in ts:
416
+ config.package_manager = "pnpm"
417
+ elif "yarn" in ts:
418
+ config.package_manager = "yarn"
419
+
420
+ if "pytest" in ts:
421
+ config.test_framework = "pytest"
422
+ elif "jest" in ts:
423
+ config.test_framework = "jest"
424
+ elif "vitest" in ts:
425
+ config.test_framework = "vitest"
426
+
427
+ # Map gates to lint_tools (filter to lint-related gates only)
428
+ if cf_config.gates:
429
+ lint_gate_names = {"ruff", "pylint", "eslint", "prettier", "flake8", "mypy", "biome"}
430
+ config.lint_tools = [g for g in cf_config.gates if g in lint_gate_names]
431
+
432
+ # Map hooks
433
+ if cf_config.hooks:
434
+ valid_hook_fields = {f.name for f in HooksConfig.__dataclass_fields__.values()}
435
+ filtered = {k: v for k, v in cf_config.hooks.items() if k in valid_hook_fields}
436
+ config.hooks = HooksConfig(**filtered)
437
+
438
+ # Map batch
439
+ if cf_config.batch:
440
+ batch_kwargs: dict[str, Any] = {}
441
+ if "max_parallel" in cf_config.batch:
442
+ batch_kwargs["max_parallel"] = cf_config.batch["max_parallel"]
443
+ if batch_kwargs:
444
+ config.batch = BatchConfig(**batch_kwargs)
445
+
446
+ # Map agent budget
447
+ if cf_config.agent:
448
+ budget_kwargs: dict[str, Any] = {}
449
+ if "max_iterations" in cf_config.agent:
450
+ budget_kwargs["max_iterations"] = cf_config.agent["max_iterations"]
451
+ # Also set base_iterations to match (they're conceptually the same)
452
+ budget_kwargs["base_iterations"] = cf_config.agent["max_iterations"]
453
+ if budget_kwargs:
454
+ config.agent_budget = AgentBudgetConfig(**budget_kwargs)
455
+
456
+ return config
457
+
458
+
459
+ def save_environment_config(workspace_path: Path, config: EnvironmentConfig) -> None:
460
+ """Save environment configuration to workspace.
461
+
462
+ Args:
463
+ workspace_path: Path to the workspace root
464
+ config: Configuration to save
465
+ """
466
+ config_dir = workspace_path / ".codeframe"
467
+ config_dir.mkdir(parents=True, exist_ok=True)
468
+
469
+ config_file = config_dir / ENV_CONFIG_FILE
470
+
471
+ with open(config_file, "w") as f:
472
+ yaml.dump(
473
+ config.to_dict(),
474
+ f,
475
+ default_flow_style=False,
476
+ sort_keys=False,
477
+ allow_unicode=True,
478
+ )
479
+
480
+
481
+ def get_default_environment_config() -> EnvironmentConfig:
482
+ """Get default environment configuration.
483
+
484
+ Returns:
485
+ EnvironmentConfig with sensible defaults
486
+ """
487
+ return EnvironmentConfig()
488
+
489
+
490
+ # =============================================================================
491
+ # v1 Legacy Configuration (JSON-based)
492
+ # =============================================================================
493
+
494
+
495
+ class ProviderConfig(BaseModel):
496
+ """LLM provider configuration."""
497
+
498
+ lead_agent: str = "claude"
499
+ backend_agent: str = "claude"
500
+ frontend_agent: str = "gpt4"
501
+ test_agent: str = "claude"
502
+ review_agent: str = "gpt4"
503
+
504
+
505
+ class AgentPolicyConfig(BaseModel):
506
+ """Global agent management policies."""
507
+
508
+ require_review_below_maturity: str = "supporting"
509
+ allow_full_autonomy: bool = False
510
+
511
+
512
+ class InterruptionConfig(BaseModel):
513
+ """Interruption mode configuration."""
514
+
515
+ enabled: bool = True
516
+ sync_blockers: list[str] = Field(default_factory=lambda: ["requirement", "security"])
517
+ async_blockers: list[str] = Field(default_factory=lambda: ["technical", "external"])
518
+ auto_continue: bool = True
519
+
520
+
521
+ class NotificationChannelConfig(BaseModel):
522
+ """Notification channel configuration."""
523
+
524
+ enabled: bool = True
525
+ channels: list[str] = Field(default_factory=list)
526
+ webhook_url: Optional[str] = None
527
+ batch_interval: Optional[int] = None
528
+
529
+
530
+ class NotificationsConfig(BaseModel):
531
+ """Multi-channel notification configuration."""
532
+
533
+ sync_blockers: NotificationChannelConfig = Field(default_factory=NotificationChannelConfig)
534
+ async_blockers: NotificationChannelConfig = Field(default_factory=NotificationChannelConfig)
535
+
536
+
537
+ class CheckpointConfig(BaseModel):
538
+ """Checkpoint configuration."""
539
+
540
+ auto_save_interval: int = 1800 # seconds
541
+ pre_compactification: bool = True
542
+ per_task_completion: bool = True
543
+
544
+
545
+ class ProjectConfig(BaseModel):
546
+ """Project-specific configuration."""
547
+
548
+ project_name: str
549
+ project_type: str = "python"
550
+ providers: ProviderConfig = Field(default_factory=ProviderConfig)
551
+ agent_policy: AgentPolicyConfig = Field(default_factory=AgentPolicyConfig)
552
+ interruption_mode: InterruptionConfig = Field(default_factory=InterruptionConfig)
553
+ notifications: NotificationsConfig = Field(default_factory=NotificationsConfig)
554
+ checkpoints: CheckpointConfig = Field(default_factory=CheckpointConfig)
555
+
556
+
557
+ class GlobalConfig(BaseSettings):
558
+ """Global CodeFRAME configuration loaded from environment variables."""
559
+
560
+ # API Keys (REQUIRED for Sprint 1)
561
+ anthropic_api_key: Optional[str] = Field(None, alias="ANTHROPIC_API_KEY")
562
+ openai_api_key: Optional[str] = Field(None, alias="OPENAI_API_KEY")
563
+
564
+ # Notification services (optional, for future sprints)
565
+ twilio_account_sid: Optional[str] = Field(None, alias="TWILIO_ACCOUNT_SID")
566
+ twilio_auth_token: Optional[str] = Field(None, alias="TWILIO_AUTH_TOKEN")
567
+
568
+ # Blocker webhook notifications (Sprint 6 - Human in the Loop)
569
+ blocker_webhook_url: Optional[str] = Field(None, alias="BLOCKER_WEBHOOK_URL")
570
+
571
+ # Database configuration
572
+ database_path: str = Field(".codeframe/state.db", alias="DATABASE_PATH")
573
+
574
+ # Status Server configuration
575
+ api_host: str = Field("0.0.0.0", alias="API_HOST")
576
+ api_port: int = Field(8080, alias="API_PORT")
577
+ cors_origins: str = Field("http://localhost:3000,http://localhost:5173", alias="CORS_ORIGINS")
578
+
579
+ # Logging configuration
580
+ log_level: str = Field("INFO", alias="LOG_LEVEL")
581
+ log_file: Optional[str] = Field(".codeframe/logs/codeframe.log", alias="LOG_FILE")
582
+
583
+ # Development flags
584
+ debug: bool = Field(False, alias="DEBUG")
585
+ hot_reload: bool = Field(False, alias="HOT_RELOAD")
586
+
587
+ # Provider defaults
588
+ default_provider: str = "claude"
589
+ default_model: str = "claude-sonnet-4"
590
+
591
+ # GitHub Integration (Sprint 11 - PR Management)
592
+ github_token: Optional[str] = Field(None, alias="GITHUB_TOKEN")
593
+ github_repo: Optional[str] = Field(None, alias="GITHUB_REPO") # Format: "owner/repo"
594
+
595
+ # Rate Limiting Configuration
596
+ rate_limit_enabled: bool = Field(True, alias="RATE_LIMIT_ENABLED")
597
+ rate_limit_storage: str = Field("memory", alias="RATE_LIMIT_STORAGE")
598
+ redis_url: Optional[str] = Field(None, alias="REDIS_URL")
599
+ rate_limit_auth: str = Field("10/minute", alias="RATE_LIMIT_AUTH")
600
+ rate_limit_standard: str = Field("100/minute", alias="RATE_LIMIT_STANDARD")
601
+ rate_limit_ai: str = Field("20/minute", alias="RATE_LIMIT_AI")
602
+ rate_limit_websocket: str = Field("30/minute", alias="RATE_LIMIT_WEBSOCKET")
603
+ # Comma-separated list of trusted proxy IPs/CIDRs (e.g., "10.0.0.0/8,172.16.0.0/12")
604
+ rate_limit_trusted_proxies: str = Field("", alias="RATE_LIMIT_TRUSTED_PROXIES")
605
+
606
+ model_config = SettingsConfigDict(
607
+ env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
608
+ )
609
+
610
+ @field_validator("log_level")
611
+ @classmethod
612
+ def validate_log_level(cls, v: str) -> str:
613
+ """Validate log level is one of the allowed values."""
614
+ allowed = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
615
+ v_upper = v.upper()
616
+ if v_upper not in allowed:
617
+ raise ValueError(f"LOG_LEVEL must be one of {allowed}, got: {v}")
618
+ return v_upper
619
+
620
+ @field_validator("api_port")
621
+ @classmethod
622
+ def validate_port(cls, v: int) -> int:
623
+ """Validate port is in valid range."""
624
+ if not (1 <= v <= 65535):
625
+ raise ValueError(f"API_PORT must be between 1 and 65535, got: {v}")
626
+ return v
627
+
628
+ @field_validator("rate_limit_storage")
629
+ @classmethod
630
+ def validate_rate_limit_storage(cls, v: str) -> str:
631
+ """Validate rate limit storage is valid."""
632
+ allowed = ["memory", "redis"]
633
+ if v not in allowed:
634
+ raise ValueError(f"RATE_LIMIT_STORAGE must be one of {allowed}, got: {v}")
635
+ return v
636
+
637
+ def get_cors_origins_list(self) -> list[str]:
638
+ """Parse CORS origins from comma-separated string."""
639
+ return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
640
+
641
+ def validate_required_for_sprint(self, sprint: int = 1) -> None:
642
+ """Validate that required configuration is present for a given sprint.
643
+
644
+ Args:
645
+ sprint: Sprint number (1-8)
646
+
647
+ Raises:
648
+ ValueError: If required configuration is missing
649
+ """
650
+ errors = []
651
+
652
+ if sprint >= 1:
653
+ # Sprint 1 requires Anthropic API key for Lead Agent
654
+ if not self.anthropic_api_key:
655
+ errors.append(
656
+ "ANTHROPIC_API_KEY is required for Sprint 1 (Lead Agent with Claude).\n"
657
+ " Get your API key at: https://console.anthropic.com/\n"
658
+ " Then add it to your .env file (see .env.example)"
659
+ )
660
+
661
+ if sprint >= 4:
662
+ # Sprint 4+ may require OpenAI for multi-agent
663
+ if not self.openai_api_key and not self.anthropic_api_key:
664
+ errors.append(
665
+ "At least one AI provider API key is required.\n"
666
+ " ANTHROPIC_API_KEY or OPENAI_API_KEY must be set."
667
+ )
668
+
669
+ if sprint >= 5:
670
+ # Sprint 5+ may require notification services
671
+ pass # Notifications are optional, will use webhook fallback
672
+
673
+ if errors:
674
+ error_msg = "\n\n".join(errors)
675
+ raise ValueError(
676
+ f"\n{'='*70}\nCONFIGURATION ERROR\n{'='*70}\n\n{error_msg}\n\n{'='*70}\n"
677
+ )
678
+
679
+ def ensure_directories(self) -> None:
680
+ """Ensure required directories exist."""
681
+ # Create database directory
682
+ db_path = Path(self.database_path)
683
+ db_path.parent.mkdir(parents=True, exist_ok=True)
684
+
685
+ # Create log directory if log file is specified
686
+ if self.log_file:
687
+ log_path = Path(self.log_file)
688
+ log_path.parent.mkdir(parents=True, exist_ok=True)
689
+
690
+
691
+ def load_environment(env_file: str = ".env") -> None:
692
+ """Load environment variables from .env file.
693
+
694
+ Args:
695
+ env_file: Path to .env file (default: .env in current directory)
696
+ """
697
+ env_path = Path(env_file)
698
+ if env_path.exists():
699
+ load_dotenv(env_path)
700
+ # Also try project root .env if we're in a subdirectory
701
+ if not env_path.is_absolute():
702
+ root_env = Path.cwd() / ".env"
703
+ if root_env.exists() and root_env != env_path.absolute():
704
+ load_dotenv(root_env)
705
+
706
+
707
+ class Config:
708
+ """Configuration manager for CodeFRAME."""
709
+
710
+ def __init__(self, project_dir: Path):
711
+ self.project_dir = project_dir
712
+ self.config_dir = project_dir / ".codeframe"
713
+ self.config_file = self.config_dir / "config.json"
714
+ self._project_config: Optional[ProjectConfig] = None
715
+ self._global_config: Optional[GlobalConfig] = None
716
+
717
+ # Load environment variables
718
+ load_environment()
719
+
720
+ def load(self) -> ProjectConfig:
721
+ """Load project configuration."""
722
+ if self._project_config:
723
+ return self._project_config
724
+
725
+ if not self.config_file.exists():
726
+ raise FileNotFoundError(f"Config not found: {self.config_file}")
727
+
728
+ with open(self.config_file) as f:
729
+ data = json.load(f)
730
+ self._project_config = ProjectConfig(**data)
731
+
732
+ return self._project_config
733
+
734
+ def save(self, config: ProjectConfig) -> None:
735
+ """Save project configuration."""
736
+ self.config_dir.mkdir(parents=True, exist_ok=True)
737
+ with open(self.config_file, "w") as f:
738
+ json.dump(config.model_dump(), f, indent=2)
739
+ self._project_config = config
740
+
741
+ def get_global(self) -> GlobalConfig:
742
+ """Load global configuration from environment variables.
743
+
744
+ Returns:
745
+ GlobalConfig instance with values from environment
746
+
747
+ Raises:
748
+ ValueError: If required configuration is missing
749
+ """
750
+ if not self._global_config:
751
+ self._global_config = GlobalConfig()
752
+ return self._global_config
753
+
754
+ def validate_for_sprint(self, sprint: int = 1) -> None:
755
+ """Validate configuration for a specific sprint.
756
+
757
+ Args:
758
+ sprint: Sprint number to validate for
759
+
760
+ Raises:
761
+ ValueError: If required configuration is missing
762
+ """
763
+ global_config = self.get_global()
764
+ global_config.validate_required_for_sprint(sprint)
765
+ global_config.ensure_directories()
766
+
767
+ def set(self, key: str, value: Any) -> None:
768
+ """Set configuration value using dot notation."""
769
+ config = self.load()
770
+ keys = key.split(".")
771
+ obj = config.model_dump()
772
+
773
+ # Navigate to the correct nested dict
774
+ current = obj
775
+ for k in keys[:-1]:
776
+ if k not in current:
777
+ current[k] = {}
778
+ current = current[k]
779
+
780
+ # Set the value
781
+ current[keys[-1]] = value
782
+
783
+ # Reload and save
784
+ self._project_config = ProjectConfig(**obj)
785
+ self.save(self._project_config)
786
+
787
+ def get(self, key: str) -> Any:
788
+ """Get configuration value using dot notation."""
789
+ config = self.load()
790
+ keys = key.split(".")
791
+ obj = config.model_dump()
792
+
793
+ current = obj
794
+ for k in keys:
795
+ if k not in current:
796
+ return None
797
+ current = current[k]
798
+
799
+ return current
800
+
801
+
802
+ # Module-level singleton for GlobalConfig
803
+ _global_config: Optional[GlobalConfig] = None
804
+
805
+
806
+ def get_global_config() -> GlobalConfig:
807
+ """Get the global configuration singleton.
808
+
809
+ Loads from environment variables on first call, cached thereafter.
810
+ This is the recommended way to access GlobalConfig for most use cases.
811
+
812
+ Returns:
813
+ GlobalConfig instance with values from environment
814
+ """
815
+ global _global_config
816
+ if _global_config is None:
817
+ _global_config = GlobalConfig()
818
+ return _global_config
819
+
820
+
821
+ def reset_global_config() -> None:
822
+ """Reset the global configuration singleton.
823
+
824
+ Useful for testing to ensure clean state between tests.
825
+ """
826
+ global _global_config
827
+ _global_config = None