diff --git a/features/agent_skills_loader.feature b/features/agent_skills_loader.feature index fe8c8b3e0..b63324042 100644 --- a/features/agent_skills_loader.feature +++ b/features/agent_skills_loader.feature @@ -295,6 +295,7 @@ Feature: Agent Skills Loader Step 1: do this. Step 2: do that. """ + When I load the agent skills folder When I call discover on the agent skill Then the discover result should have name "local/disclosure-skill" And the discover result should have description "Progressive disclosure test skill." @@ -311,6 +312,7 @@ Feature: Agent Skills Loader Step 1: do this. Step 2: do that. """ + When I load the agent skills folder When I call discover on the agent skill And I call activate on the agent skill Then the activate result body should contain "Step 1: do this." @@ -327,6 +329,7 @@ Feature: Agent Skills Loader Use the scripts. """ And the folder contains a script file "helper.py" with content "# helper" + When I load the agent skills folder When I call discover on the agent skill And I list available resources Then the resources list should include "helper.py" @@ -341,6 +344,7 @@ Feature: Agent Skills Loader --- Step 1: do this. """ + When I load the agent skills folder When I call discover on the agent skill And I call activate on the agent skill And I call deactivate on the agent skill @@ -690,6 +694,7 @@ Feature: Agent Skills Loader Instructions. """ And the folder contains an asset file "config.json" with content "{}" + When I load the agent skills folder When I call discover on the agent skill Then the discover result should have 1 resource slot(s) And the discover result resource slot "assets" should have access "read_only" diff --git a/scripts/check-adr-compliance.py b/scripts/check-adr-compliance.py new file mode 100644 index 000000000..99da11d5c --- /dev/null +++ b/scripts/check-adr-compliance.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""ADR compliance checker for CleverAgents. + +Verifies that code follows the architectural decisions recorded in ADRs. + +ADR-002: Asyncio Concurrency Model + - Async functions should use 'async def', not threading + - No direct thread usage in application layer + +ADR-003: Dependency Injection Framework + - Services should receive dependencies via __init__ + - No direct instantiation of infrastructure in application layer + +ADR-007: Repository Pattern for Persistence + - Database access only through repository classes + - No direct SQLAlchemy session usage in services + +Usage: + python scripts/check-adr-compliance.py +""" + +import ast +import sys +from pathlib import Path + + +def check_adr002_async_model(src_dir: Path) -> list[str]: + """Check ADR-002: Asyncio Concurrency Model. + + Verifies no direct threading usage in application code. + """ + violations: list[str] = [] + app_dir = src_dir / "application" + if not app_dir.exists(): + return violations + + for py_file in app_dir.rglob("*.py"): + content = py_file.read_text() + if "import threading" in content or "from threading import" in content: + violations.append( + f"ADR-002 violation: {py_file.relative_to(src_dir)} " + "imports threading directly. Use asyncio instead." + ) + return violations + + +def check_adr003_dependency_injection(src_dir: Path) -> list[str]: + """Check ADR-003: Dependency Injection Framework. + + Verifies services use constructor injection. + """ + violations: list[str] = [] + services_dir = src_dir / "application" / "services" + if not services_dir.exists(): + return violations + + for py_file in services_dir.rglob("*.py"): + if py_file.name.startswith("__"): + continue + try: + tree = ast.parse(py_file.read_text()) + except SyntaxError: + continue + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + # Check if class has __init__ with parameters (DI) + has_init = False + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "__init__": + has_init = True + # Check for at least one parameter beyond self + if len(item.args.args) < 2: + violations.append( + f"ADR-003 note: {py_file.relative_to(src_dir)}:" + f"{node.lineno} class {node.name} has __init__ " + "with no injected dependencies" + ) + if not has_init and node.body: + # Services without __init__ may be okay if they're utility classes + pass + return violations + + +def check_adr007_repository_pattern(src_dir: Path) -> list[str]: + """Check ADR-007: Repository Pattern for Persistence. + + Verifies no direct SQLAlchemy session usage in service layer. + """ + violations: list[str] = [] + services_dir = src_dir / "application" / "services" + if not services_dir.exists(): + return violations + + for py_file in services_dir.rglob("*.py"): + content = py_file.read_text() + # Check for direct session imports + if "from sqlalchemy" in content: + violations.append( + f"ADR-007 violation: {py_file.relative_to(src_dir)} " + "imports SQLAlchemy directly. Use repository pattern instead." + ) + if "session.query" in content or "session.execute" in content: + violations.append( + f"ADR-007 violation: {py_file.relative_to(src_dir)} " + "uses session directly. Access data through repositories." + ) + return violations + + +def main() -> int: + """Run all ADR compliance checks.""" + src_dir = Path("src/cleveragents") + if not src_dir.exists(): + print("Error: src/cleveragents directory not found") + return 1 + + all_violations: list[str] = [] + + print("Checking ADR-002: Asyncio Concurrency Model...") + all_violations.extend(check_adr002_async_model(src_dir)) + + print("Checking ADR-003: Dependency Injection Framework...") + all_violations.extend(check_adr003_dependency_injection(src_dir)) + + print("Checking ADR-007: Repository Pattern for Persistence...") + all_violations.extend(check_adr007_repository_pattern(src_dir)) + + if all_violations: + print(f"\nFound {len(all_violations)} ADR compliance issue(s):") + for violation in all_violations: + print(f" - {violation}") + return 1 + + print("\nAll ADR compliance checks passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())