""" Project detection and build/test tools. These mirror Loki's .shared/utils.sh detect_project() heuristic and the sisyphus/coder tools.sh run_build / run_tests / verify_build commands. Loki Concept Mapping: Loki uses a heuristic cascade: check for Cargo.toml → go.mod → package.json etc., then falls back to an LLM call for unknown projects. We replicate the heuristic portion here. The LLM fallback is omitted since the agents themselves can reason about unknown project types. """ from __future__ import annotations import json import os import subprocess from langchain_core.tools import tool # --------------------------------------------------------------------------- # Project detection (mirrors _detect_heuristic in utils.sh) # --------------------------------------------------------------------------- _HEURISTICS: list[tuple[str, dict[str, str]]] = [ ("Cargo.toml", {"type": "rust", "build": "cargo build", "test": "cargo test", "check": "cargo check"}), ("go.mod", {"type": "go", "build": "go build ./...", "test": "go test ./...", "check": "go vet ./..."}), ("package.json", {"type": "nodejs", "build": "npm run build", "test": "npm test", "check": "npm run lint"}), ("pyproject.toml", {"type": "python", "build": "", "test": "pytest", "check": "ruff check ."}), ("pom.xml", {"type": "java", "build": "mvn compile", "test": "mvn test", "check": "mvn verify"}), ("Makefile", {"type": "make", "build": "make build", "test": "make test", "check": "make lint"}), ] def detect_project(directory: str) -> dict[str, str]: """Detect project type and return build/test commands.""" for marker, info in _HEURISTICS: if os.path.exists(os.path.join(directory, marker)): return info return {"type": "unknown", "build": "", "test": "", "check": ""} @tool def get_project_info(directory: str = ".") -> str: """Detect the project type and show structure overview. Args: directory: Project root directory. """ directory = os.path.expanduser(directory) info = detect_project(directory) result = f"Project: {os.path.abspath(directory)}\n" result += f"Type: {info['type']}\n" result += f"Build: {info['build'] or '(none)'}\n" result += f"Test: {info['test'] or '(none)'}\n" result += f"Check: {info['check'] or '(none)'}\n" return result def _run_project_command(directory: str, command_key: str) -> str: """Run a detected project command (build/test/check).""" directory = os.path.expanduser(directory) info = detect_project(directory) cmd = info.get(command_key, "") if not cmd: return f"No {command_key} command detected for this project." try: result = subprocess.run( cmd, shell=True, capture_output=True, text=True, cwd=directory, timeout=300, ) output = result.stdout + result.stderr status = "SUCCESS" if result.returncode == 0 else f"FAILED (exit {result.returncode})" return f"Running: {cmd}\n\n{output}\n\n{command_key.upper()}: {status}" except subprocess.TimeoutExpired: return f"{command_key.upper()}: TIMEOUT after 300s" except Exception as e: return f"{command_key.upper()}: ERROR — {e}" @tool def run_build(directory: str = ".") -> str: """Run the project's build command. Args: directory: Project root directory. """ return _run_project_command(directory, "build") @tool def run_tests(directory: str = ".") -> str: """Run the project's test suite. Args: directory: Project root directory. """ return _run_project_command(directory, "test") @tool def verify_build(directory: str = ".") -> str: """Run the project's check/lint command to verify correctness. Args: directory: Project root directory. """ return _run_project_command(directory, "check") @tool def execute_command(command: str, directory: str = ".") -> str: """Execute a shell command and return its output. Args: command: Shell command to execute. directory: Working directory. """ directory = os.path.expanduser(directory) try: result = subprocess.run( command, shell=True, capture_output=True, text=True, cwd=directory, timeout=120, ) output = (result.stdout + result.stderr).strip() if result.returncode != 0: return f"Command failed (exit {result.returncode}):\n{output}" return output or "(no output)" except subprocess.TimeoutExpired: return "Command timed out after 120s." except Exception as e: return f"Error: {e}"