codeframe-ai 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|