Land HARNESS-WP-0001 T01/T02/T04/T05: extend ADR-005 schedule.yml with harness fields, named tool-profile registry, ADR-004 metrics writes, and BudgetTracker wiring. CLI gains validate/profiles; task-file path kept.
97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
"""Agentic Claude Code adapter.
|
|
|
|
llm-connect's ClaudeCodeAdapter is a text-generation adapter (`claude
|
|
--print`, no working directory, no tool grants). An executor run needs an
|
|
*agentic* session: file edits and git commits inside the target repo, under
|
|
a hard tool allow-list. This adapter subclasses it, keeping the llm-connect
|
|
LLMAdapter interface so a hosted adapter can be swapped in later, and adds:
|
|
|
|
- cwd pinned to the target repo
|
|
- --permission-mode acceptEdits
|
|
- allow-list from a named tool profile (default: green-commit-only)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from llm_connect.claude_code import ClaudeCodeAdapter
|
|
from llm_connect.exceptions import LLMSubprocessError, LLMTimeoutError
|
|
from llm_connect.models import LLMResponse, RunConfig
|
|
|
|
from agent_harness.profiles import ToolProfile, get_profile
|
|
|
|
# Backward-compatible alias for the seed profile allow-list string.
|
|
ALLOWED_TOOLS = get_profile("green-commit-only").allowed_tools
|
|
|
|
|
|
class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
|
|
def __init__(
|
|
self,
|
|
workdir: Path,
|
|
*,
|
|
tool_profile: str | ToolProfile = "green-commit-only",
|
|
**kwargs,
|
|
):
|
|
super().__init__(**kwargs)
|
|
self._workdir = workdir
|
|
if isinstance(tool_profile, ToolProfile):
|
|
self._profile = tool_profile
|
|
else:
|
|
self._profile = get_profile(tool_profile)
|
|
|
|
@property
|
|
def tool_profile(self) -> ToolProfile:
|
|
return self._profile
|
|
|
|
def _build_command(self, config: RunConfig) -> list[str]:
|
|
cmd = [
|
|
self._cli_path,
|
|
"--print",
|
|
"--permission-mode",
|
|
"acceptEdits",
|
|
"--allowedTools",
|
|
self._profile.allowed_tools,
|
|
]
|
|
if self._model:
|
|
cmd.extend(["--model", self._model])
|
|
return cmd
|
|
|
|
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
|
self._preflight_budget(config)
|
|
cmd = self._build_command(config)
|
|
timeout = config.timeout_seconds or self._config.timeout_seconds
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
input=prompt,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
cwd=self._workdir,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
raise LLMTimeoutError(
|
|
f"claude CLI timed out after {timeout}s", cause=exc
|
|
) from exc
|
|
if result.returncode != 0:
|
|
raise LLMSubprocessError(
|
|
f"claude CLI exited with code {result.returncode}",
|
|
return_code=result.returncode,
|
|
stderr=result.stderr,
|
|
)
|
|
response = LLMResponse(
|
|
content=result.stdout,
|
|
model=self._model or "claude-code-cli",
|
|
usage={},
|
|
finish_reason="stop",
|
|
metadata={
|
|
"provider": "claude-code-agentic",
|
|
"cli_path": self._cli_path,
|
|
"workdir": str(self._workdir),
|
|
"tool_profile": self._profile.name,
|
|
},
|
|
)
|
|
self._consume_budget(config, response)
|
|
return response
|