Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

291 lines
10 KiB
Python

"""Claude Code implementation executor.
Spawns the Claude Code CLI to implement a requested change in the current
repository, applies the execution policy, tracks the launch as a background
task, and watches the subprocess lifecycle in a daemon thread.
Lives next to the agent-facing ``tools.interactive_shell.actions.implementation``.
``subprocess`` and ``threading`` are referenced as module globals so tests can
patch ``tools.interactive_shell.implementation.claude_code_executor.subprocess.Popen`` /
``.threading.Thread``; ``ClaudeCodeAdapter`` is likewise a module global that
tests patch directly.
"""
from __future__ import annotations
import os
import subprocess
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol, cast
from integrations.llm_cli.claude_code import ClaudeCodeAdapter
from integrations.llm_cli.subprocess_env import build_cli_subprocess_env
from platform.common.task_types import TaskKind
from tools.interactive_shell.shared import allow_tool
from tools.interactive_shell.subprocess import (
CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS,
MAX_COMMAND_OUTPUT_CHARS,
SYNTHETIC_DIAG_CHARS,
SubprocessPresenter,
terminate_child_process,
)
_DIM_STYLE = "dim"
_ERROR_STYLE = "error"
_WARNING_STYLE = "warning"
_IMPLEMENT_PERMISSION_MODE_ENV = "CLAUDE_CODE_IMPLEMENT_PERMISSION_MODE"
_DEFAULT_IMPLEMENT_PERMISSION_MODE = "acceptEdits"
class ClaudeInvocation(Protocol):
@property
def argv(self) -> tuple[str, ...]:
raise NotImplementedError
@property
def stdin(self) -> str | None:
raise NotImplementedError
@property
def cwd(self) -> str:
raise NotImplementedError
@property
def env(self) -> dict[str, str] | None:
raise NotImplementedError
@dataclass(frozen=True)
class ClaudeCodeRunResult:
stdout: str
stderr: str
exit_code: int | None
timed_out: bool
cancelled: bool
def is_context_dependent_implementation_request(request: str) -> bool:
normalized = " ".join(request.strip().lower().split())
return normalized in {
"implement",
"please implement",
"code",
"make the change",
"make those changes",
}
def build_claude_code_implementation_prompt(
request: str,
*,
recent_messages: list[tuple[str, str]],
) -> str:
context = ""
if recent_messages:
context = "\n".join(f"{role}: {text}" for role, text in recent_messages)
context_block = (
f"--- Recent OpenSRE terminal assistant context ---\n{context}\n\n" if context else ""
)
return (
"You are Claude Code working in the current OpenSRE repository.\n\n"
f"{context_block}"
f"--- User implementation request ---\n{request.strip()}\n\n"
"--- Rules ---\n"
"- Implement the requested change in this repository.\n"
"- Follow AGENTS.md, existing project conventions, and local code style.\n"
"- Do not create a git commit or push changes.\n"
"- Do not run destructive git commands such as reset --hard or checkout --.\n"
"- Preserve unrelated user changes in the working tree.\n"
"- Run focused tests or lint checks when practical.\n"
"- Finish with a concise summary of changed files and verification performed.\n"
)
def implementation_argv(argv: tuple[str, ...]) -> list[str]:
exec_argv = list(argv)
if not exec_argv:
raise ValueError("Claude Code invocation is empty.")
executable = Path(exec_argv[0])
if not executable.is_absolute():
raise ValueError("Claude Code executable path must be absolute.")
permission_mode = os.environ.get(
_IMPLEMENT_PERMISSION_MODE_ENV,
_DEFAULT_IMPLEMENT_PERMISSION_MODE,
).strip()
if permission_mode and permission_mode.lower() not in {"default", "none", "off"}:
exec_argv.extend(["--permission-mode", permission_mode])
return exec_argv
def spawn_claude_code(invocation: ClaudeInvocation) -> subprocess.Popen[str]:
argv = implementation_argv(invocation.argv)
cwd = str(Path(invocation.cwd).resolve())
env = build_cli_subprocess_env(invocation.env)
popen = cast("type[subprocess.Popen[str]]", subprocess.__dict__["Popen"])
return popen(
argv,
stdin=subprocess.PIPE if invocation.stdin is not None else subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
cwd=cwd,
env=env,
start_new_session=True,
)
def run_claude_code_to_completion(
proc: subprocess.Popen[str],
invocation: ClaudeInvocation,
*,
cancel_event: threading.Event,
) -> ClaudeCodeRunResult:
timed_out = False
try:
stdout, stderr = proc.communicate(
input=invocation.stdin,
timeout=CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
timed_out = True
terminate_child_process(proc)
stdout, stderr = proc.communicate()
out = (stdout or "")[:MAX_COMMAND_OUTPUT_CHARS]
err = (stderr or "")[:MAX_COMMAND_OUTPUT_CHARS]
code = proc.returncode
cancelled = cancel_event.is_set() and code != 0
return ClaudeCodeRunResult(
stdout=out,
stderr=err,
exit_code=code,
timed_out=timed_out,
cancelled=cancelled,
)
def format_claude_failure_diag(stdout: str, stderr: str) -> str:
return (stderr or stdout).strip()[:SYNTHETIC_DIAG_CHARS]
def run_claude_code_implementation(request: str, presenter: SubprocessPresenter) -> None:
session = presenter.session
policy = allow_tool("code_agent")
if not presenter.execution_allowed(
policy,
action_summary=f"Claude Code implementation: {request}",
):
session.record("implementation", request, ok=False)
return
if is_context_dependent_implementation_request(request) and not session.agent.messages:
presenter.print(
"[error]implementation request is too vague:[/] "
"describe what Claude Code should change."
)
session.record("implementation", request, ok=False)
return
adapter = ClaudeCodeAdapter()
probe = adapter.detect()
if not probe.installed or not probe.bin_path:
presenter.print_error(f"Claude Code CLI not available: {probe.detail}")
session.record("implementation", request, ok=False)
return
if probe.logged_in is False:
presenter.print_error(f"Claude Code is not authenticated: {probe.detail}")
session.record("implementation", request, ok=False)
return
recent = session.agent.messages[-6:]
prompt = build_claude_code_implementation_prompt(request, recent_messages=recent)
try:
invocation = adapter.build(
prompt=prompt,
model=os.environ.get("CLAUDE_CODE_MODEL"),
workspace=str(Path.cwd()),
)
except Exception as exc:
presenter.report_exception(exc, context="surfaces.interactive_shell.claude_code.build")
presenter.print_error(f"Claude Code failed to prepare: {exc}")
session.record("implementation", request, ok=False)
return
display_command = "claude -p"
presenter.print_bold_command(display_command)
task = session.task_registry.create(TaskKind.CODE_AGENT, command=display_command)
task.mark_running()
history_gen_when_started = session.terminal.history_generation
try:
proc = spawn_claude_code(invocation)
except Exception as exc:
task.mark_failed(str(exc))
presenter.report_exception(exc, context="surfaces.interactive_shell.claude_code.start")
presenter.print_error(f"Claude Code failed to start: {exc}")
session.record("implementation", request, ok=False)
return
task.attach_process(proc)
session.record("implementation", request, ok=True)
def _watch() -> None:
try:
result = run_claude_code_to_completion(
proc,
invocation,
cancel_event=task.cancel_requested,
)
if result.timed_out:
task.mark_failed(f"timed out after {CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS}s")
presenter.print(
f"[error]Claude Code timed out after "
f"{CLAUDE_CODE_IMPLEMENTATION_TIMEOUT_SECONDS} seconds[/]"
)
return
if result.cancelled:
task.mark_cancelled()
if session.terminal.history_generation == history_gen_when_started:
session.mark_latest(ok=False, kind="implementation")
presenter.print(f"[{_WARNING_STYLE}]Claude Code task cancelled.[/]")
return
if result.exit_code == 0:
task.mark_completed(result="ok")
presenter.print_highlight(f"Claude Code completed task {task.task_id}")
presenter.print_command_output(result.stdout)
if result.stderr:
presenter.print_command_output(result.stderr, style=_DIM_STYLE)
return
diag = format_claude_failure_diag(result.stdout, result.stderr)
error_msg = f"exit code {result.exit_code}" + (f": {diag}" if diag else "")
task.mark_failed(error_msg)
if session.terminal.history_generation == history_gen_when_started:
session.mark_latest(ok=False, kind="implementation")
presenter.print(f"[error]Claude Code failed (exit {result.exit_code}):[/]")
presenter.print_command_output(result.stdout)
presenter.print_command_output(result.stderr, style=_ERROR_STYLE)
except Exception as exc: # noqa: BLE001
task.mark_failed(str(exc))
presenter.report_exception(exc, context="surfaces.interactive_shell.claude_code.watch")
if session.terminal.history_generation == history_gen_when_started:
session.mark_latest(ok=False, kind="implementation")
presenter.print_error(f"Claude Code watcher failed: {exc}")
threading.Thread(target=_watch, daemon=True, name=f"claude-code-{task.task_id}").start()
presenter.print(
f"[dim]Claude Code started — task[/] [bold]{task.task_id}[/bold]. "
"[highlight]/tasks[/] [dim]to monitor,[/] "
f"[highlight]/cancel {task.task_id}[/] [dim]to stop.[/]"
)
__all__ = ["run_claude_code_implementation"]