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,569 @@
1
+ """Agent preferences loader for CodeFRAME v2.
2
+
3
+ Loads project-level preferences from CODEFRAME.md, AGENTS.md, and CLAUDE.md files.
4
+ These preferences guide agent decision-making for tactical choices like
5
+ tooling, file handling, and code style.
6
+
7
+ CODEFRAME.md supports YAML front matter for typed configuration (engine, gates,
8
+ batch settings, hooks) plus a Markdown body for agent instructions.
9
+
10
+ Supports the AGENTS.md industry standard (OpenAI, Google, GitHub, Anthropic)
11
+ as well as CLAUDE.md for Anthropic-specific instructions.
12
+
13
+ This module is headless - no FastAPI or HTTP dependencies.
14
+
15
+ References:
16
+ - https://agents.md/
17
+ - https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/
18
+ """
19
+
20
+ import logging
21
+ import re
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Optional
25
+
26
+ import yaml
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class AgentPreferences:
33
+ """Project-level preferences for agent decision-making.
34
+
35
+ Attributes:
36
+ always_do: Safe autonomous actions the agent should take without asking
37
+ ask_first: Genuinely risky decisions requiring user confirmation
38
+ never_do: Forbidden actions the agent must not perform
39
+ tooling: Tool preferences (e.g., package_manager, test_runner)
40
+ code_style: Code style conventions and formatting preferences
41
+ commands: Executable commands (build, test, lint, etc.)
42
+ raw_content: Original markdown content for passing to LLM
43
+ source_files: List of files that contributed to these preferences
44
+ """
45
+
46
+ always_do: list[str] = field(default_factory=list)
47
+ ask_first: list[str] = field(default_factory=list)
48
+ never_do: list[str] = field(default_factory=list)
49
+ tooling: dict[str, str] = field(default_factory=dict)
50
+ code_style: dict[str, str] = field(default_factory=dict)
51
+ commands: dict[str, str] = field(default_factory=dict)
52
+ raw_content: str = ""
53
+ source_files: list[str] = field(default_factory=list)
54
+
55
+ def has_preferences(self) -> bool:
56
+ """Check if any preferences are loaded."""
57
+ return bool(
58
+ self.always_do
59
+ or self.ask_first
60
+ or self.never_do
61
+ or self.tooling
62
+ or self.commands
63
+ or self.raw_content
64
+ )
65
+
66
+ def to_prompt_section(self) -> str:
67
+ """Convert preferences to a prompt section for LLM.
68
+
69
+ Returns:
70
+ Formatted markdown section for inclusion in prompts
71
+ """
72
+ if not self.has_preferences():
73
+ return ""
74
+
75
+ sections = ["## Project Preferences"]
76
+
77
+ if self.tooling:
78
+ sections.append("\n### Tooling")
79
+ for key, value in self.tooling.items():
80
+ sections.append(f"- **{key}**: {value}")
81
+
82
+ if self.commands:
83
+ sections.append("\n### Commands")
84
+ for key, value in self.commands.items():
85
+ sections.append(f"- **{key}**: `{value}`")
86
+
87
+ if self.always_do:
88
+ sections.append("\n### Always Do (autonomous actions)")
89
+ for item in self.always_do:
90
+ sections.append(f"- {item}")
91
+
92
+ if self.ask_first:
93
+ sections.append("\n### Ask First (require confirmation)")
94
+ for item in self.ask_first:
95
+ sections.append(f"- {item}")
96
+
97
+ if self.never_do:
98
+ sections.append("\n### Never Do (forbidden)")
99
+ for item in self.never_do:
100
+ sections.append(f"- {item}")
101
+
102
+ if self.code_style:
103
+ sections.append("\n### Code Style")
104
+ for key, value in self.code_style.items():
105
+ sections.append(f"- **{key}**: {value}")
106
+
107
+ return "\n".join(sections)
108
+
109
+
110
+ @dataclass
111
+ class CodeframeConfig:
112
+ """Typed configuration from CODEFRAME.md YAML front matter.
113
+
114
+ Attributes:
115
+ engine: Execution engine (react, plan, claude-code, etc.)
116
+ tech_stack: Natural language tech stack description
117
+ batch: Batch execution settings (max_parallel, default_strategy)
118
+ gates: Verification gates to run (ruff, pytest, mypy, etc.)
119
+ hooks: Lifecycle hooks (before_task, after_task, on_failure, etc.)
120
+ agent: Agent tuning parameters (max_iterations, verbose, etc.)
121
+ raw: Full parsed YAML dict for extensibility
122
+ """
123
+
124
+ engine: Optional[str] = None
125
+ tech_stack: Optional[str] = None
126
+ batch: dict = field(default_factory=dict)
127
+ gates: list[str] = field(default_factory=list)
128
+ hooks: dict = field(default_factory=dict)
129
+ agent: dict = field(default_factory=dict)
130
+ raw: dict = field(default_factory=dict)
131
+
132
+
133
+ # Section header patterns for parsing AGENTS.md content
134
+ SECTION_PATTERNS = {
135
+ "always_do": re.compile(
136
+ r"#+\s*(?:always\s*do|autonomous|safe\s*actions?)", re.IGNORECASE
137
+ ),
138
+ "ask_first": re.compile(
139
+ r"#+\s*(?:ask\s*first|require\s*confirmation|risky)", re.IGNORECASE
140
+ ),
141
+ "never_do": re.compile(
142
+ r"#+\s*(?:never\s*do|forbidden|prohibited|don't|do\s*not)", re.IGNORECASE
143
+ ),
144
+ "tooling": re.compile(r"#+\s*(?:tooling|tools|preferences)", re.IGNORECASE),
145
+ "commands": re.compile(r"#+\s*(?:commands?|scripts?|build)", re.IGNORECASE),
146
+ "code_style": re.compile(r"#+\s*(?:code\s*style|style|conventions?)", re.IGNORECASE),
147
+ }
148
+
149
+
150
+ def _parse_list_items(content: str) -> list[str]:
151
+ """Extract list items from markdown content.
152
+
153
+ Args:
154
+ content: Markdown text that may contain list items
155
+
156
+ Returns:
157
+ List of extracted items (without bullet prefixes)
158
+ """
159
+ items = []
160
+ for line in content.split("\n"):
161
+ line = line.strip()
162
+ # Match bullet points: -, *, or numbered lists
163
+ if line.startswith(("-", "*", "•")):
164
+ item = line.lstrip("-*• ").strip()
165
+ if item:
166
+ items.append(item)
167
+ elif re.match(r"^\d+\.", line):
168
+ item = re.sub(r"^\d+\.\s*", "", line).strip()
169
+ if item:
170
+ items.append(item)
171
+ return items
172
+
173
+
174
+ def _parse_key_value_items(content: str) -> dict[str, str]:
175
+ """Extract key-value pairs from markdown content.
176
+
177
+ Supports formats:
178
+ - **key**: value
179
+ - `key`: value
180
+ - key: value
181
+
182
+ Args:
183
+ content: Markdown text with key-value pairs
184
+
185
+ Returns:
186
+ Dictionary of key-value pairs
187
+ """
188
+ items = {}
189
+ for line in content.split("\n"):
190
+ line = line.strip()
191
+ if not line:
192
+ continue
193
+
194
+ # Strip list markers carefully - don't strip markdown bold asterisks
195
+ # Match patterns like "- " or "* " at start (with space after)
196
+ list_marker_match = re.match(r"^[-*•]\s+", line)
197
+ if list_marker_match:
198
+ line = line[list_marker_match.end():]
199
+
200
+ # Try different key-value formats
201
+ # **key**: value
202
+ match = re.match(r"\*\*([^*]+)\*\*:\s*(.+)", line)
203
+ if match:
204
+ items[match.group(1).strip().lower().replace(" ", "_")] = match.group(
205
+ 2
206
+ ).strip()
207
+ continue
208
+
209
+ # `key`: value
210
+ match = re.match(r"`([^`]+)`:\s*(.+)", line)
211
+ if match:
212
+ items[match.group(1).strip().lower().replace(" ", "_")] = match.group(
213
+ 2
214
+ ).strip()
215
+ continue
216
+
217
+ # key: value (simple format)
218
+ if ":" in line:
219
+ parts = line.split(":", 1)
220
+ if len(parts) == 2:
221
+ key = parts[0].strip().lower().replace(" ", "_")
222
+ value = parts[1].strip()
223
+ if key and value:
224
+ items[key] = value
225
+
226
+ return items
227
+
228
+
229
+ def _split_by_sections(content: str) -> dict[str, str]:
230
+ """Split markdown content by section headers.
231
+
232
+ Args:
233
+ content: Full markdown content
234
+
235
+ Returns:
236
+ Dictionary mapping section names to their content
237
+ """
238
+ sections = {}
239
+ current_section = None
240
+ current_content = []
241
+
242
+ for line in content.split("\n"):
243
+ # Check if this is a header line
244
+ if line.startswith("#"):
245
+ # Save previous section
246
+ if current_section:
247
+ sections[current_section] = "\n".join(current_content)
248
+
249
+ # Identify new section
250
+ current_section = None
251
+ for section_name, pattern in SECTION_PATTERNS.items():
252
+ if pattern.search(line):
253
+ current_section = section_name
254
+ current_content = []
255
+ break
256
+
257
+ if current_section is None:
258
+ # Unknown section, capture as raw
259
+ current_section = "unknown"
260
+ current_content = [line]
261
+ else:
262
+ current_content.append(line)
263
+
264
+ # Save final section
265
+ if current_section:
266
+ sections[current_section] = "\n".join(current_content)
267
+
268
+ return sections
269
+
270
+
271
+ def _parse_agents_md(content: str) -> AgentPreferences:
272
+ """Parse AGENTS.md content into structured preferences.
273
+
274
+ Args:
275
+ content: Raw markdown content from AGENTS.md
276
+
277
+ Returns:
278
+ Parsed AgentPreferences
279
+ """
280
+ prefs = AgentPreferences(raw_content=content)
281
+
282
+ sections = _split_by_sections(content)
283
+
284
+ if "always_do" in sections:
285
+ prefs.always_do = _parse_list_items(sections["always_do"])
286
+
287
+ if "ask_first" in sections:
288
+ prefs.ask_first = _parse_list_items(sections["ask_first"])
289
+
290
+ if "never_do" in sections:
291
+ prefs.never_do = _parse_list_items(sections["never_do"])
292
+
293
+ if "tooling" in sections:
294
+ prefs.tooling = _parse_key_value_items(sections["tooling"])
295
+
296
+ if "commands" in sections:
297
+ prefs.commands = _parse_key_value_items(sections["commands"])
298
+
299
+ if "code_style" in sections:
300
+ prefs.code_style = _parse_key_value_items(sections["code_style"])
301
+
302
+ return prefs
303
+
304
+
305
+ def parse_codeframe_md(content: str) -> tuple[CodeframeConfig, AgentPreferences]:
306
+ """Parse a CODEFRAME.md file with YAML front matter + Markdown body.
307
+
308
+ The file format is:
309
+ ---
310
+ engine: react
311
+ tech_stack: "Python with uv"
312
+ batch:
313
+ max_parallel: 4
314
+ gates:
315
+ - ruff
316
+ - pytest
317
+ hooks:
318
+ before_task: "git checkout -b cf/{{task_id}}"
319
+ agent:
320
+ max_iterations: 30
321
+ ---
322
+
323
+ # Project Instructions
324
+ Markdown content here becomes the agent system prompt supplement...
325
+
326
+ Returns:
327
+ Tuple of (CodeframeConfig, AgentPreferences)
328
+ CodeframeConfig has the typed YAML settings
329
+ AgentPreferences has the Markdown body as raw_content + any extracted sections
330
+ """
331
+ config = CodeframeConfig()
332
+ prefs = AgentPreferences()
333
+
334
+ if not content or not content.strip():
335
+ return config, prefs
336
+
337
+ # Extract YAML front matter between --- markers
338
+ yaml_data = {}
339
+ body = content
340
+
341
+ stripped = content.strip()
342
+ if stripped.startswith("---"):
343
+ # Find the closing --- marker (skip the opening one)
344
+ after_opening = stripped[3:]
345
+ closing_idx = after_opening.find("\n---")
346
+ if closing_idx >= 0:
347
+ yaml_text = after_opening[:closing_idx].strip()
348
+ # Body starts after the closing ---
349
+ rest = after_opening[closing_idx + 4:] # skip "\n---"
350
+ body = rest.strip()
351
+
352
+ # Parse YAML
353
+ try:
354
+ parsed = yaml.safe_load(yaml_text)
355
+ if isinstance(parsed, dict):
356
+ yaml_data = parsed
357
+ except yaml.YAMLError:
358
+ logger.warning("Invalid YAML front matter in CODEFRAME.md, skipping")
359
+ body = content # Treat entire content as body on YAML failure
360
+ else:
361
+ # No closing --- found, treat entire content as body
362
+ body = content
363
+
364
+ # Build CodeframeConfig from YAML
365
+ if yaml_data:
366
+ config = CodeframeConfig(
367
+ engine=yaml_data.get("engine"),
368
+ tech_stack=yaml_data.get("tech_stack"),
369
+ batch=yaml_data.get("batch") or {},
370
+ gates=yaml_data.get("gates") or [],
371
+ hooks=yaml_data.get("hooks") or {},
372
+ agent=yaml_data.get("agent") or {},
373
+ raw=yaml_data,
374
+ )
375
+
376
+ # Parse Markdown body for AgentPreferences
377
+ if body:
378
+ prefs = _parse_agents_md(body)
379
+ prefs.source_files = ["CODEFRAME.md"]
380
+
381
+ # Cross-populate tech_stack into preferences tooling
382
+ if config.tech_stack:
383
+ prefs.tooling["tech_stack"] = config.tech_stack
384
+
385
+ return config, prefs
386
+
387
+
388
+ def _merge_preferences(
389
+ base: AgentPreferences, override: AgentPreferences
390
+ ) -> AgentPreferences:
391
+ """Merge two preference objects, with override taking precedence.
392
+
393
+ Args:
394
+ base: Base preferences
395
+ override: Overriding preferences (higher priority)
396
+
397
+ Returns:
398
+ Merged AgentPreferences
399
+ """
400
+ return AgentPreferences(
401
+ always_do=override.always_do if override.always_do else base.always_do,
402
+ ask_first=override.ask_first if override.ask_first else base.ask_first,
403
+ never_do=override.never_do if override.never_do else base.never_do,
404
+ tooling={**base.tooling, **override.tooling},
405
+ code_style={**base.code_style, **override.code_style},
406
+ commands={**base.commands, **override.commands},
407
+ raw_content=override.raw_content or base.raw_content,
408
+ source_files=base.source_files + override.source_files,
409
+ )
410
+
411
+
412
+ def load_preferences(workspace_path: Path) -> AgentPreferences:
413
+ """Load and merge agent preferences from CODEFRAME.md, AGENTS.md, and CLAUDE.md.
414
+
415
+ Search order (closest wins):
416
+ 1. workspace_path/CODEFRAME.md (highest priority)
417
+ 2. workspace_path/AGENTS.md
418
+ 3. workspace_path/CLAUDE.md (lowest priority)
419
+ 4. Parent directories (walking up, same precedence order)
420
+ 5. ~/.codeframe/AGENTS.md (global defaults)
421
+
422
+ At each directory level, CODEFRAME.md > AGENTS.md > CLAUDE.md.
423
+
424
+ Args:
425
+ workspace_path: Path to the workspace/repository root
426
+
427
+ Returns:
428
+ Merged AgentPreferences from all found files
429
+ """
430
+ workspace_path = Path(workspace_path).resolve()
431
+ prefs = AgentPreferences()
432
+ found_files = []
433
+
434
+ # Search order: global defaults first, then walk up to workspace (closest wins)
435
+ search_paths: list[tuple[Path, bool]] = [] # (path, is_codeframe_md)
436
+
437
+ # 1. Global defaults (lowest priority)
438
+ global_config = Path.home() / ".codeframe" / "AGENTS.md"
439
+ if global_config.exists():
440
+ search_paths.append((global_config, False))
441
+
442
+ # 2. Walk from root to workspace (so workspace files override parents)
443
+ current = workspace_path
444
+ path_chain = []
445
+ while current != current.parent:
446
+ path_chain.append(current)
447
+ current = current.parent
448
+
449
+ # Reverse so we go from ancestors to workspace (closest wins)
450
+ for dir_path in reversed(path_chain):
451
+ # CLAUDE.md first (lowest priority at same level)
452
+ claude_md = dir_path / "CLAUDE.md"
453
+ if claude_md.exists():
454
+ search_paths.append((claude_md, False))
455
+
456
+ # AGENTS.md second (medium priority at same level)
457
+ agents_md = dir_path / "AGENTS.md"
458
+ if agents_md.exists():
459
+ search_paths.append((agents_md, False))
460
+
461
+ # CODEFRAME.md third (highest priority at same level)
462
+ codeframe_md = dir_path / "CODEFRAME.md"
463
+ if codeframe_md.exists():
464
+ search_paths.append((codeframe_md, True))
465
+
466
+ # Process all found files
467
+ for file_path, is_codeframe in search_paths:
468
+ try:
469
+ content = file_path.read_text(encoding="utf-8")
470
+ if is_codeframe:
471
+ _, file_prefs = parse_codeframe_md(content)
472
+ file_prefs.source_files = [str(file_path)]
473
+ else:
474
+ file_prefs = _parse_agents_md(content)
475
+ file_prefs.source_files = [str(file_path)]
476
+ prefs = _merge_preferences(prefs, file_prefs)
477
+ found_files.append(str(file_path))
478
+ except (OSError, UnicodeDecodeError):
479
+ # Log but don't fail on file read errors
480
+ pass
481
+
482
+ # If no config files were found, use sensible defaults
483
+ if not prefs.has_preferences():
484
+ return get_default_preferences()
485
+
486
+ return prefs
487
+
488
+
489
+ def get_default_preferences() -> AgentPreferences:
490
+ """Get sensible default preferences when no AGENTS.md exists.
491
+
492
+ These defaults encourage autonomous decision-making for tactical choices
493
+ while preserving caution for genuinely risky operations.
494
+
495
+ Returns:
496
+ Default AgentPreferences
497
+ """
498
+ return AgentPreferences(
499
+ always_do=[
500
+ "Choose between equivalent implementation approaches",
501
+ "Decide file organization and naming",
502
+ "Select library versions (prefer latest stable)",
503
+ "Handle existing files (overwrite, merge, or extend as appropriate)",
504
+ "Choose test frameworks and configurations",
505
+ "Make code style decisions following existing patterns",
506
+ "Install dependencies using the detected package manager",
507
+ "Create directories as needed",
508
+ "Fix linting errors automatically",
509
+ ],
510
+ ask_first=[
511
+ "Delete critical configuration files",
512
+ "Make breaking API changes",
513
+ "Modify security-sensitive code without explicit request",
514
+ "Remove or disable tests",
515
+ "Change database schemas in production",
516
+ ],
517
+ never_do=[
518
+ "Commit secrets, API keys, or credentials",
519
+ "Delete .git directory or history",
520
+ "Push to main/master without explicit permission",
521
+ "Modify files outside the workspace",
522
+ "Execute commands that could cause data loss",
523
+ "Bypass security checks or validation",
524
+ ],
525
+ tooling={
526
+ "package_manager": "detect from lockfiles (uv > pip for Python, npm > yarn for JS)",
527
+ "test_runner": "detect from project (pytest for Python, jest for JS)",
528
+ "linter": "detect from project (ruff for Python, eslint for JS)",
529
+ },
530
+ commands={
531
+ "python_install": "uv sync",
532
+ "python_test": "uv run pytest",
533
+ "python_lint": "uv run ruff check --fix .",
534
+ "js_install": "npm install",
535
+ "js_test": "npm test",
536
+ "js_lint": "npm run lint",
537
+ },
538
+ raw_content="",
539
+ source_files=["<defaults>"],
540
+ )
541
+
542
+
543
+ def get_codeframe_config(workspace_path: Path) -> Optional[CodeframeConfig]:
544
+ """Load CodeframeConfig from CODEFRAME.md if present.
545
+
546
+ Searches for CODEFRAME.md starting from workspace_path, walking up to root.
547
+ Returns None if no CODEFRAME.md is found.
548
+
549
+ Args:
550
+ workspace_path: Path to the workspace/repository root
551
+
552
+ Returns:
553
+ CodeframeConfig if CODEFRAME.md is found, None otherwise
554
+ """
555
+ workspace_path = Path(workspace_path).resolve()
556
+
557
+ current = workspace_path
558
+ while current != current.parent:
559
+ codeframe_md = current / "CODEFRAME.md"
560
+ if codeframe_md.exists():
561
+ try:
562
+ content = codeframe_md.read_text(encoding="utf-8")
563
+ config, _ = parse_codeframe_md(content)
564
+ return config
565
+ except (OSError, UnicodeDecodeError):
566
+ return None
567
+ current = current.parent
568
+
569
+ return None