Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ AUTO_APPROVE_IGNORE_LOW_SEVERITY=false

# [optional] Built-in types use presets in code — leave ACP_COMMAND/ACP_ARGS unset.
#
# claude → npx -y @zed-industries/claude-agent-acp
# codex → npx -y @zed-industries/codex-acp
# claude → npx -y @agentclientprotocol/claude-agent-acp
# codex → npx -y @agentclientprotocol/codex-acp
# opencode → opencode acp
#
# Custom type: set ACP_AGENT_TYPE plus ACP_COMMAND and ACP_ARGS.
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ its own built-in knowledge. The Docker image pre-installs the public code-review

### 1. Install dependencies

Requires **Python 3.12+** and [uv](https://docs.astral.sh/uv/) (see `.python-version`; Docker image uses `python:3.12-slim`).
Requires **Python 3.12+** and [uv](https://docs.astral.sh/uv/) (see `.python-version`; Docker image
uses `python:3.12-slim`). Local npx launchers require Node.js; Claude ACP requires **Node.js 22+**.
Docker images already include Node.js 22.

```bash
uv sync --extra dev
Expand Down Expand Up @@ -143,6 +145,9 @@ they are never embedded in workspace Git URLs.
| `LOG_LEVEL` | no | `INFO` | Bot log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
| `LOG_DIR` | no | `logs` | Root directory for bot-generated files (relative to `CODE_REVIEW_BOT_ROOT`). Fixed subdirs: `sessions/` (per-review logs), `debug-reports/` (`--debug` Markdown reports). Empty string disables file logging. |

After ACP initialization, INFO logs include the agent-reported name/version, protocol version, and
launcher command; no additional package-manager lookup is performed.

### Runtime paths

| Variable | Required | Default | Description |
Expand Down
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ RUN if [ -n "$APT_MIRROR" ]; then \
curl \
gnupg \
ripgrep \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

Expand Down Expand Up @@ -82,7 +82,7 @@ USER root
ARG NPM_CONFIG_REGISTRY
RUN set -eu; \
if [ -n "$NPM_CONFIG_REGISTRY" ]; then npm config set registry "$NPM_CONFIG_REGISTRY"; fi; \
npm install -g @zed-industries/claude-agent-acp; \
npm install -g @agentclientprotocol/claude-agent-acp; \
npm list -g --depth=0
USER appuser
ENV ACP_AGENT_TYPE=claude
5 changes: 3 additions & 2 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ docker build -f docker/Dockerfile \
Select the `claude` or `opencode` target to install only that coding agent. A build without
`--target` uses the final `claude` stage. Package versions are intentionally unpinned, so a rebuilt
npm layer resolves the current registry version; `npm list -g --depth=0` records the resolved
version in the build log. A cached layer retains its installed version.
version in the build log. A cached layer retains its installed version. Both images include
Node.js 22; the Docker host does not need Node.js installed.

## Coding-agent image tags

Expand All @@ -48,7 +49,7 @@ whhe/code-review-bot:opencode

Each target persists its matching `ACP_AGENT_TYPE`. Both variants leave `ACP_COMMAND` and
`ACP_ARGS` unset. Built-in presets start Claude through
`npx -y @zed-industries/claude-agent-acp` and OpenCode through `opencode acp`. Agent-specific
`npx -y @agentclientprotocol/claude-agent-acp` and OpenCode through `opencode acp`. Agent-specific
Docker stages ensure Claude images do not contain OpenCode-only environment defaults.

`:latest` and `:claude-code` are aliases for the same Claude image. OpenCode is published only as
Expand Down
11 changes: 10 additions & 1 deletion src/code_review_bot/agent/acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ async def request_permission(
),
) as (conn, _proc):
try:
await conn.initialize(
initialize_response = await conn.initialize(
protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(),
client_info=Implementation(
Expand All @@ -248,6 +248,15 @@ async def request_permission(
version="0.1.0",
),
)
agent_info = getattr(initialize_response, "agent_info", None)
protocol_version = getattr(initialize_response, "protocol_version", None)
logger.info(
"[agent runtime] name=%s version=%s protocol=%s command=%s",
getattr(agent_info, "name", None) or "(unknown)",
getattr(agent_info, "version", None) or "(unknown)",
protocol_version if protocol_version is not None else "(unknown)",
config.command,
)
session_kwargs: dict[str, Any] = {"cwd": cwd}
if additional_directories:
session_kwargs["additional_directories"] = additional_directories
Expand Down
4 changes: 2 additions & 2 deletions src/code_review_bot/agent/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
BUILTIN_ACP_AGENT_TYPES: frozenset[str] = frozenset({"claude", "codex", "opencode"})

ACP_PRESETS: dict[BuiltinAcpAgentType, tuple[str, list[str]]] = {
"claude": ("npx", ["-y", "@zed-industries/claude-agent-acp"]),
"codex": ("npx", ["-y", "@zed-industries/codex-acp"]),
"claude": ("npx", ["-y", "@agentclientprotocol/claude-agent-acp"]),
"codex": ("npx", ["-y", "@agentclientprotocol/codex-acp"]),
"opencode": ("opencode", ["acp"]),
}

Expand Down
2 changes: 1 addition & 1 deletion tests/test_acp_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_is_builtin() -> None:
def test_builtin_uses_preset() -> None:
command, args = resolve_acp_launcher("codex")
assert command == "npx"
assert args == ["-y", "@zed-industries/codex-acp"]
assert args == ["-y", "@agentclientprotocol/codex-acp"]


def test_opencode_is_builtin_and_uses_native_acp() -> None:
Expand Down
55 changes: 53 additions & 2 deletions tests/test_acp_runtime.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
Expand All @@ -13,9 +14,10 @@ class _Connection:
def __init__(self) -> None:
self.model_calls: list[tuple[str, str]] = []
self.config_calls: list[tuple[str, str, str]] = []
self.initialize_response = SimpleNamespace(agent_info=None, protocol_version=1)

async def initialize(self, **kwargs: object) -> None:
return None
async def initialize(self, **kwargs: object) -> SimpleNamespace:
return self.initialize_response

async def new_session(self, **kwargs: object) -> SimpleNamespace:
return SimpleNamespace(session_id="session-1")
Expand Down Expand Up @@ -115,3 +117,52 @@ async def fake_spawn_agent_process(
run = await agent.run_once("review")

assert run.model == "provider/runtime-model"


@pytest.mark.asyncio
@pytest.mark.parametrize(
("agent_info", "command", "expected_log"),
[
(
SimpleNamespace(name="claude-agent-acp", version="0.60.0"),
"npx",
"[agent runtime] name=claude-agent-acp version=0.60.0 protocol=1 command=npx",
),
(
None,
"agent",
"[agent runtime] name=(unknown) version=(unknown) protocol=1 command=agent",
),
],
)
async def test_acp_runtime_logs_agent_identity_from_initialize_response(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
agent_info: SimpleNamespace | None,
command: str,
expected_log: str,
) -> None:
connection = _Connection()
connection.initialize_response = SimpleNamespace(
agent_info=agent_info,
protocol_version=1,
)

@asynccontextmanager
async def fake_spawn_agent_process(
client: object,
command: str,
*args: str,
env: dict[str, str] | None = None,
**kwargs: object,
) -> AsyncIterator[tuple[_Connection, SimpleNamespace]]:
yield connection, SimpleNamespace(stdin=None, stdout=None, stderr=None, _transport=None)

monkeypatch.setattr(acp_sdk, "spawn_agent_process", fake_spawn_agent_process)
agent = AcpCodingAgent(AcpAgentConfig(command=command, verbose=False), tmp_path)

with caplog.at_level(logging.INFO, logger="code_review_bot.agent.acp"):
await agent.run_once("review")

assert expected_log in caplog.messages
4 changes: 2 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_settings_acp_agent_type_defaults_to_claude(monkeypatch: pytest.MonkeyPa
settings = Settings(_env_file=None)
assert settings.acp_agent_type == "claude"
assert settings.resolved_acp_command == "npx"
assert settings.resolved_acp_args == ["-y", "@zed-industries/claude-agent-acp"]
assert settings.resolved_acp_args == ["-y", "@agentclientprotocol/claude-agent-acp"]


def test_settings_acp_agent_type_codex_launcher(monkeypatch: pytest.MonkeyPatch) -> None:
Expand All @@ -124,7 +124,7 @@ def test_settings_acp_agent_type_codex_launcher(monkeypatch: pytest.MonkeyPatch)

settings = Settings(_env_file=None)
assert settings.acp_agent_type == "codex"
assert settings.resolved_acp_args == ["-y", "@zed-industries/codex-acp"]
assert settings.resolved_acp_args == ["-y", "@agentclientprotocol/codex-acp"]


def test_settings_acp_agent_type_opencode_launcher(monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_opencode_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def test_shared_dockerfile_installs_only_selected_agent() -> None:

dockerfile = path.read_text(encoding="utf-8")
assert "FROM python:3.12-slim AS base" in dockerfile
assert "https://deb.nodesource.com/setup_22.x" in dockerfile
assert "FROM base AS opencode" in dockerfile
assert "FROM base AS claude" in dockerfile
assert "FROM agent-${ACP_AGENT_TYPE}" not in dockerfile
Expand All @@ -28,7 +29,7 @@ def test_shared_dockerfile_installs_only_selected_agent() -> None:
"FROM base AS claude", 1
)[0]
claude_stage = dockerfile.split("FROM base AS claude", 1)[1]
assert "npm install -g @zed-industries/claude-agent-acp" in claude_stage
assert "npm install -g @agentclientprotocol/claude-agent-acp" in claude_stage
assert "ENV ACP_AGENT_TYPE=claude" in claude_stage
assert "OPENCODE_" not in claude_stage
assert "npm install -g opencode-ai" in opencode_stage
Expand Down