Sprint 4D: AGENTS.md parser
Browse files
purpose_agent/protocols/agents_md.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
agents_md.py — Parser for repo-local AGENTS.md instruction files.
|
| 3 |
+
|
| 4 |
+
Precedence: system_policy > developer_config > AGENTS.md > team_purpose > skill_cards > user_task
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import logging
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class AgentsConfig:
|
| 16 |
+
instructions: list[str] = field(default_factory=list)
|
| 17 |
+
capabilities: dict[str, str] = field(default_factory=dict)
|
| 18 |
+
constraints: list[str] = field(default_factory=list)
|
| 19 |
+
metadata: dict[str, Any] = field(default_factory=dict)
|
| 20 |
+
source_path: str = ""
|
| 21 |
+
def to_prompt_section(self) -> str:
|
| 22 |
+
parts = []
|
| 23 |
+
if self.instructions:
|
| 24 |
+
parts.append("## Repository Instructions (AGENTS.md)")
|
| 25 |
+
for i in self.instructions: parts.append(f"- {i}")
|
| 26 |
+
if self.constraints:
|
| 27 |
+
parts.append("\n## Constraints")
|
| 28 |
+
for c in self.constraints: parts.append(f"- {c}")
|
| 29 |
+
return "\n".join(parts)
|
| 30 |
+
@property
|
| 31 |
+
def has_content(self) -> bool:
|
| 32 |
+
return bool(self.instructions or self.capabilities or self.constraints)
|
| 33 |
+
|
| 34 |
+
def parse_agents_md(content: str, source_path: str = "") -> AgentsConfig:
|
| 35 |
+
config = AgentsConfig(source_path=source_path)
|
| 36 |
+
if content.startswith("---"):
|
| 37 |
+
end = content.find("---", 3)
|
| 38 |
+
if end > 0: content = content[end+3:]
|
| 39 |
+
current = None
|
| 40 |
+
for line in content.split("\n"):
|
| 41 |
+
line = line.strip()
|
| 42 |
+
if line.startswith("## ") or line.startswith("# "):
|
| 43 |
+
h = line.lstrip("#").strip().lower()
|
| 44 |
+
if "instruction" in h: current = "instructions"
|
| 45 |
+
elif "capabilit" in h: current = "capabilities"
|
| 46 |
+
elif "constraint" in h: current = "constraints"
|
| 47 |
+
else: current = None
|
| 48 |
+
continue
|
| 49 |
+
if not line or line.startswith("<!--"): continue
|
| 50 |
+
item = line.lstrip("- ").lstrip("* ").strip()
|
| 51 |
+
if not item: continue
|
| 52 |
+
if current == "instructions": config.instructions.append(item)
|
| 53 |
+
elif current == "capabilities":
|
| 54 |
+
if ":" in item:
|
| 55 |
+
n, d = item.split(":", 1); config.capabilities[n.strip()] = d.strip()
|
| 56 |
+
else: config.capabilities[item] = ""
|
| 57 |
+
elif current == "constraints": config.constraints.append(item)
|
| 58 |
+
return config
|
| 59 |
+
|
| 60 |
+
def load_agents_md(path: str = "AGENTS.md") -> AgentsConfig | None:
|
| 61 |
+
for p in [Path(path), Path(".github/AGENTS.md"), Path("docs/AGENTS.md")]:
|
| 62 |
+
if p.exists():
|
| 63 |
+
try:
|
| 64 |
+
config = parse_agents_md(p.read_text(), str(p))
|
| 65 |
+
if config.has_content: return config
|
| 66 |
+
except: pass
|
| 67 |
+
return None
|