diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c852979e..f3c40a04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,6 +99,7 @@ jobs: [roboco-agent-doc]=docker/agent-doc.Dockerfile [roboco-agent-prompter]=docker/agent-prompter.Dockerfile [roboco-agent-secretary]=docker/agent-secretary.Dockerfile + [roboco-agent-pr-reviewer]=docker/agent-pr-reviewer.Dockerfile ) for name in "${!IMAGES[@]}"; do echo "::group::build ${name}" diff --git a/docker-compose.registry.yml b/docker-compose.registry.yml index 4a0f9203..d3982852 100644 --- a/docker-compose.registry.yml +++ b/docker-compose.registry.yml @@ -155,6 +155,11 @@ services: entrypoint: ["/bin/sh", "-c", "echo 'agent-secretary image present'"] restart: "no" + agent-pr-reviewer-image: + image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-pr-reviewer:${ROBOCO_VERSION:-latest} + entrypoint: ["/bin/sh", "-c", "echo 'agent-pr-reviewer image present'"] + restart: "no" + # -------------------------------------------------------------------------- # Orchestrator — API server + agent spawner # -------------------------------------------------------------------------- diff --git a/docker-compose.yaml b/docker-compose.yaml index 5b96a1ea..404d1cdc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -216,6 +216,19 @@ services: depends_on: - agent-base-image + # ========================================================================== + # Agent PR Reviewer Image Builder (read-only reviewer of inbound external PRs) + # ========================================================================== + agent-pr-reviewer-image: + build: + context: . + dockerfile: docker/agent-pr-reviewer.Dockerfile + image: roboco-agent-pr-reviewer + entrypoint: ["/bin/sh", "-c", 'echo "Agent PR Reviewer image built"'] + restart: "no" + depends_on: + - agent-base-image + # ========================================================================== # Orchestrator - API Server + Agent Spawner # ========================================================================== diff --git a/docker-compose.yml b/docker-compose.yml index 5b96a1ea..404d1cdc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -216,6 +216,19 @@ services: depends_on: - agent-base-image + # ========================================================================== + # Agent PR Reviewer Image Builder (read-only reviewer of inbound external PRs) + # ========================================================================== + agent-pr-reviewer-image: + build: + context: . + dockerfile: docker/agent-pr-reviewer.Dockerfile + image: roboco-agent-pr-reviewer + entrypoint: ["/bin/sh", "-c", 'echo "Agent PR Reviewer image built"'] + restart: "no" + depends_on: + - agent-base-image + # ========================================================================== # Orchestrator - API Server + Agent Spawner # ========================================================================== diff --git a/docker/agent-pr-reviewer.Dockerfile b/docker/agent-pr-reviewer.Dockerfile new file mode 100644 index 00000000..3653d296 --- /dev/null +++ b/docker/agent-pr-reviewer.Dockerfile @@ -0,0 +1,15 @@ +# PR Reviewer Agent +# +# A read-only reviewer: fetches a PR diff via the GitHub API, greps the +# codebase, and posts one change-request. It never runs or builds the code (the +# trust boundary — execution only happens later in a dev cell during supersede), +# so it needs no language toolchain or test runner beyond the base image. It +# gets its own image for parity with every other agent and so the role is +# explicit on the compose + release surface. Keeps the base `claude` entrypoint +# — it is dispatched per review task like the dev/QA agents, not a persistent +# SDK driver like intake/secretary. + +FROM roboco-agent-base + +LABEL role="pr-reviewer" +LABEL description="Read-only PR reviewer - reviews inbound external PRs and posts one change-request" diff --git a/roboco/runtime/orchestrator.py b/roboco/runtime/orchestrator.py index be5bd61c..77d0592f 100644 --- a/roboco/runtime/orchestrator.py +++ b/roboco/runtime/orchestrator.py @@ -123,11 +123,10 @@ AGENT_IMAGES: dict[str, str] = { "product-owner": "roboco-agent-pm", "head-marketing": "roboco-agent-pm", "auditor": "roboco-agent-pm", - # PR Reviewer — read-only reviewer (fetches the PR diff via API, greps the - # code, posts one change-request; never runs code). The base image has - # everything it needs, so it intentionally reuses it rather than shipping a - # dedicated image. Explicit (not a silent fallback) so the choice is visible. - "pr-reviewer-1": AGENT_BASE_IMAGE, + # PR Reviewer — read-only reviewer (diff via API, grep, post one + # change-request; never runs code). Its own image for parity with the other + # agents; built FROM the base, no extra toolchain. + "pr-reviewer-1": "roboco-agent-pr-reviewer", # Intake — persistent Agent-SDK driver, not a one-shot `claude -p`. INTAKE_AGENT_ID: "roboco-agent-prompter", # Secretary — persistent Agent-SDK driver with gated CEO authority. @@ -787,6 +786,7 @@ class AgentOrchestrator: "roboco-agent-ux": "agent-ux.Dockerfile", "roboco-agent-prompter": "agent-prompter.Dockerfile", "roboco-agent-secretary": "agent-secretary.Dockerfile", + "roboco-agent-pr-reviewer": "agent-pr-reviewer.Dockerfile", } dockerfile = dockerfile_map.get(bare) if dockerfile: diff --git a/tests/unit/runtime/test_agent_image_registry.py b/tests/unit/runtime/test_agent_image_registry.py index 43bb81a2..65dd13d1 100644 --- a/tests/unit/runtime/test_agent_image_registry.py +++ b/tests/unit/runtime/test_agent_image_registry.py @@ -53,8 +53,10 @@ def test_get_agent_image_local_default(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(orch.settings, "agent_image_registry", "") monkeypatch.setattr(orch.settings, "agent_image_tag", "") assert orch.get_agent_image("be-dev-1") == "roboco-agent-dev-be" - # Unknown agent id falls back to the base image. - assert orch.get_agent_image("pr-reviewer-1") == "roboco-agent-base" + # The PR reviewer has its own image (parity with the other agents). + assert orch.get_agent_image("pr-reviewer-1") == "roboco-agent-pr-reviewer" + # A genuinely unknown agent id falls back to the base image. + assert orch.get_agent_image("nope-not-real") == "roboco-agent-base" def test_get_agent_image_registry_mode(monkeypatch: pytest.MonkeyPatch) -> None: @@ -66,5 +68,5 @@ def test_get_agent_image_registry_mode(monkeypatch: pytest.MonkeyPatch) -> None: ) assert ( orch.get_agent_image("pr-reviewer-1") - == "ghcr.io/rennf93/roboco-agent-base:0.5.0" + == "ghcr.io/rennf93/roboco-agent-pr-reviewer:0.5.0" )