From a6ee057a8f22bd9cd3f75000585f6254cffd4a46 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 10 Aug 2026 13:03:54 -0400 Subject: [PATCH 01/10] feat: add aforge harness option --- .env.example | 2 ++ README.md | 11 ++++++++++- agentfield-package.yaml | 4 +++- go/README.md | 10 +++++++++- go/agentfield-package.yaml | 4 ++-- go/cmd/pr-af/main.go | 2 +- go/go.mod | 2 +- go/go.sum | 4 ++-- go/internal/harnessx/run.go | 2 +- go/internal/node/node_test.go | 2 ++ pyproject.toml | 5 ++++- src/pr_af/app.py | 3 ++- src/pr_af/config.py | 7 +++++++ tests/test_aforge_config.py | 22 ++++++++++++++++++++++ 14 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 tests/test_aforge_config.py diff --git a/.env.example b/.env.example index 5e59cda..8a777f8 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ AGENTFIELD_PUBLIC_URL= NODE_ID=pr-af # --- AI / harness config --- +# `aforge` selects the unreleased aforge-v2 CLI through AgentField's harness. PR_AF_PROVIDER=opencode PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 # Falls back to PR_AF_MODEL when unset @@ -24,6 +25,7 @@ PR_AF_AI_MAX_RETRIES=3 PR_AF_AI_INITIAL_BACKOFF_SECONDS=2.0 PR_AF_AI_MAX_BACKOFF_SECONDS=8.0 PR_AF_OPENCODE_BIN=opencode +PR_AF_AFORGE_BIN=aforge # Optional provider-agnostic harness executable override (leave unset to use provider defaults) # PR_AF_HARNESS_BIN= PR_AF_OPENCODE_SERVER= diff --git a/README.md b/README.md index d88dff2..0b1c2c1 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,16 @@ The key knobs (see `.env.example` for the full list): |-----------------------------|----------------------------------------------------------------| | `OPENROUTER_API_KEY` | LLM provider key (OpenRouter) — required | | `GH_TOKEN` | GitHub token (`repo` scope) for reading PRs and posting reviews | -| `PR_AF_PROVIDER` | Harness provider (default `opencode`) | +| `PR_AF_PROVIDER` | Harness provider (default `opencode`; accepts `aforge`) | +| `PR_AF_AFORGE_BIN` | Path to an aforge-v2 binary (default `aforge`) | +| `PR_AF_HARNESS_BIN` | Provider-agnostic executable override | + +For an unreleased aforge-v2 source benchmark, build `./cmd/aforge`, then run +PR-AF with `PR_AF_PROVIDER=aforge` and either `PR_AF_AFORGE_BIN` (Python node) +or `PR_AF_HARNESS_BIN` (maintained Go node) set to the absolute binary path. +The draft pins the exact AgentField harness commit used for benchmarking; +replace that pin with the released SDK before merging. The current Docker +images do not bundle the unreleased Aforge binary. | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_MAX_COST_USD` | Per-run cost ceiling in USD (default `2.0`) | | `PR_AF_MAX_DURATION_SECONDS`| Per-run wall-clock ceiling in seconds (default `3600`) | diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 88f20b5..36e904c 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -49,8 +49,10 @@ user_environment: type: secret scope: global - name: PR_AF_PROVIDER - description: Coding-agent harness provider + description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: opencode + - name: PR_AF_AFORGE_BIN + description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) - name: PR_AF_MODEL description: Model the harness uses default: openrouter/moonshotai/kimi-k2.5 diff --git a/go/README.md b/go/README.md index 854716d..87d8fc9 100644 --- a/go/README.md +++ b/go/README.md @@ -155,7 +155,15 @@ The node is configured entirely through the environment. | `AGENTFIELD_API_KEY` | Control-plane API key (if the CP has auth enabled) | | `NODE_ID` | Node ID (default `pr-af`) | | `PORT` | Listen port (default `8007`) | -| `PR_AF_PROVIDER` | Harness provider (default `opencode`) | +| `PR_AF_PROVIDER` | Harness provider (default `opencode`; accepts `aforge`) | +| `PR_AF_HARNESS_BIN` | Provider executable override (set to the aforge-v2 binary) | + +To benchmark unreleased aforge-v2 without changing PR-AF prompts, build +`aforge-v2/cmd/aforge`, then set `PR_AF_PROVIDER=aforge` and +`PR_AF_HARNESS_BIN=/absolute/path/to/aforge`. The draft pins the exact +AgentField Go SDK commit used for benchmarking; replace that pin with the +released SDK before merging. The current Docker image does not bundle the +unreleased Aforge binary. | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_LABEL` | Pull-request label that triggers a webhook review (default `pr-af`) | | `PR_AF_MAX_CONCURRENT_REVIEWERS` | Optional webhook review concurrency cap (minimum `1`) | diff --git a/go/agentfield-package.yaml b/go/agentfield-package.yaml index ed0b8de..abcadd2 100644 --- a/go/agentfield-package.yaml +++ b/go/agentfield-package.yaml @@ -41,7 +41,7 @@ user_environment: type: secret scope: global - name: PR_AF_PROVIDER - description: harness provider + description: harness provider (aforge | claude-code | codex | gemini | opencode) default: opencode - name: PR_AF_MODEL description: harness model @@ -56,7 +56,7 @@ user_environment: - name: PR_AF_MAX_COVERAGE_ITERATIONS description: optional coverage iteration cap for webhook-triggered reviews (minimum 1) - name: PR_AF_HARNESS_BIN - description: optional executable override for every harness provider (unset uses provider defaults) + description: optional executable override for every harness provider (set to an aforge-v2 binary with PR_AF_PROVIDER=aforge) - name: PR_AF_MAX_COST_USD description: per-run cost ceiling default: "2.0" diff --git a/go/cmd/pr-af/main.go b/go/cmd/pr-af/main.go index f350cf2..7fb8cec 100644 --- a/go/cmd/pr-af/main.go +++ b/go/cmd/pr-af/main.go @@ -13,7 +13,7 @@ // AGENT_CALLBACK_URL base URL the CP uses to reach this node (else localhost) // NODE_ID node id (default pr-af) // PORT listen port (default 8007) -// PR_AF_PROVIDER harness provider (default opencode) +// PR_AF_PROVIDER harness provider (default opencode; accepts aforge) // PR_AF_MODEL harness model (env wins over the code default) // PR_AF_HARNESS_BIN optional executable override for every harness provider // OPENROUTER_API_KEY LLM key — required for the .ai() gates; AIConfig is only diff --git a/go/go.mod b/go/go.mod index 339de0a..8862f99 100644 --- a/go/go.mod +++ b/go/go.mod @@ -5,7 +5,7 @@ module github.com/Agent-Field/pr-af/go go 1.21 require ( - github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260714191100-2cc5fe2adcf4 + github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/invopop/jsonschema v0.13.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 diff --git a/go/go.sum b/go/go.sum index 92df3d9..826be15 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,5 @@ -github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260714191100-2cc5fe2adcf4 h1:B3uCMLZSa2rsRDRGuecBIXCgkYqBuScSK4CXKPDaCgk= -github.com/Agent-Field/agentfield/sdk/go v0.0.0-20260714191100-2cc5fe2adcf4/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= +github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58 h1:B9ITli2PwuEG+lv7md3CuFAiCnTcuhNIJ6hjNQVTQqQ= +github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= diff --git a/go/internal/harnessx/run.go b/go/internal/harnessx/run.go index cf0397b..fd8c911 100644 --- a/go/internal/harnessx/run.go +++ b/go/internal/harnessx/run.go @@ -80,7 +80,7 @@ func seedDefaults[T any]() T { // router.harness (system_prompt, schema, model, provider, tools, cwd, max_turns, // permission_mode). type RoleOptions struct { - // Provider is the harness ADAPTER string, e.g. "opencode" (PR-AF's default), + // Provider is the harness ADAPTER string, e.g. "aforge" or "opencode" (PR-AF's default), // "claude-code", "codex". Provider string diff --git a/go/internal/node/node_test.go b/go/internal/node/node_test.go index 0b3fe4c..9011475 100644 --- a/go/internal/node/node_test.go +++ b/go/internal/node/node_test.go @@ -16,6 +16,8 @@ func TestHarnessConfigProviderAwareBinary(t *testing.T) { }{ {"codex uses SDK default", config.AIIntegrationConfig{Provider: "codex", OpencodeBin: "C:/bin/opencode-custom"}, ""}, {"opencode uses configured binary", config.AIIntegrationConfig{Provider: "opencode", OpencodeBin: "C:/bin/opencode-custom"}, "C:/bin/opencode-custom"}, + {"aforge uses SDK default", config.AIIntegrationConfig{Provider: "aforge", OpencodeBin: "C:/bin/opencode-custom"}, ""}, + {"generic override selects aforge", config.AIIntegrationConfig{Provider: "aforge", HarnessBin: "C:/bin/aforge-custom"}, "C:/bin/aforge-custom"}, {"generic override wins", config.AIIntegrationConfig{Provider: "codex", OpencodeBin: "C:/bin/opencode-custom", HarnessBin: "C:/bin/provider-custom"}, "C:/bin/provider-custom"}, {"generic override wins for opencode", config.AIIntegrationConfig{Provider: "opencode", OpencodeBin: "C:/bin/opencode-custom", HarnessBin: "C:/bin/provider-custom"}, "C:/bin/provider-custom"}, } diff --git a/pyproject.toml b/pyproject.toml index d9c5449..4df6134 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield>=0.1.84", + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python", "hax-sdk>=0.2.4", "pydantic>=2.0", "httpx>=0.27", @@ -36,6 +36,9 @@ pr-af = "pr_af.app:main" [tool.hatch.build.targets.wheel] packages = ["src/pr_af"] +[tool.hatch.metadata] +allow-direct-references = true + [tool.ruff] target-version = "py311" line-length = 120 diff --git a/src/pr_af/app.py b/src/pr_af/app.py index 87e736b..f58a8bb 100644 --- a/src/pr_af/app.py +++ b/src/pr_af/app.py @@ -43,7 +43,8 @@ model=_ai_config.harness_model, max_turns=_ai_config.max_turns, env=_ai_config.provider_env(), - opencode_bin=_ai_config.opencode_bin, + opencode_bin=_ai_config.harness_bin or _ai_config.opencode_bin, + aforge_bin=_ai_config.harness_bin or _ai_config.aforge_bin, permission_mode="auto", ), ai_config=AIConfig( diff --git a/src/pr_af/config.py b/src/pr_af/config.py index 90991f8..df74e1c 100644 --- a/src/pr_af/config.py +++ b/src/pr_af/config.py @@ -330,6 +330,13 @@ class AIIntegrationConfig(BaseModel): ) max_backoff_seconds: float = Field(default_factory=lambda: float(os.getenv("PR_AF_AI_MAX_BACKOFF_SECONDS", "8.0"))) opencode_bin: str = Field(default_factory=lambda: os.getenv("PR_AF_OPENCODE_BIN", "opencode")) + aforge_bin: str = Field( + default_factory=lambda: os.getenv( + "PR_AF_AFORGE_BIN", + os.getenv("AFORGE_BIN", "aforge"), + ) + ) + harness_bin: str = Field(default_factory=lambda: os.getenv("PR_AF_HARNESS_BIN", "")) opencode_server: str | None = Field(default_factory=lambda: os.getenv("PR_AF_OPENCODE_SERVER")) @classmethod diff --git a/tests/test_aforge_config.py b/tests/test_aforge_config.py new file mode 100644 index 0000000..6918c26 --- /dev/null +++ b/tests/test_aforge_config.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from pr_af.config import AIIntegrationConfig + + +def test_aforge_provider_and_binary_overrides(monkeypatch) -> None: + monkeypatch.setenv("PR_AF_PROVIDER", "aforge") + monkeypatch.setenv("PR_AF_AFORGE_BIN", "/opt/aforge") + + config = AIIntegrationConfig.from_env() + + assert config.provider == "aforge" + assert config.aforge_bin == "/opt/aforge" + assert config.harness_bin == "" + + +def test_generic_harness_binary_is_available_to_all_providers(monkeypatch) -> None: + monkeypatch.setenv("PR_AF_HARNESS_BIN", "/opt/harness") + + config = AIIntegrationConfig.from_env() + + assert config.harness_bin == "/opt/harness" From f3a5a3a2c2e78364446007d78ca0b14b34af9e57 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 14 Aug 2026 23:38:38 -0400 Subject: [PATCH 02/10] feat: make aforge exec the default harness --- .env.example | 5 +++-- Dockerfile | 10 ++++++++-- README.md | 15 +++++++-------- agentfield-package.yaml | 5 ++++- docker-compose.go.yml | 3 ++- docker-compose.yml | 3 ++- go/Dockerfile | 9 +++++++-- go/README.md | 13 +++++-------- go/agentfield-package.yaml | 5 ++++- go/cmd/pr-af/main.go | 2 +- go/go.mod | 2 +- go/go.sum | 2 ++ go/internal/config/ai.go | 3 ++- go/internal/config/config_test.go | 7 +++++-- go/internal/harnessx/run.go | 2 +- go/internal/node/node.go | 2 +- go/internal/node/node_test.go | 6 +++++- go/test/functional/compose.functional.yml | 3 ++- pyproject.toml | 2 +- src/pr_af/config.py | 3 ++- tests/test_aforge_config.py | 16 ++++++++++++++++ 21 files changed, 81 insertions(+), 37 deletions(-) diff --git a/.env.example b/.env.example index 8a777f8..dc933af 100644 --- a/.env.example +++ b/.env.example @@ -15,8 +15,9 @@ AGENTFIELD_PUBLIC_URL= NODE_ID=pr-af # --- AI / harness config --- -# `aforge` selects the unreleased aforge-v2 CLI through AgentField's harness. -PR_AF_PROVIDER=opencode +# AForge exec is the default. Set PR_AF_PROVIDER=opencode to roll back. +PR_AF_PROVIDER=aforge +AGENTFIELD_AFORGE_COMMAND=exec PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 # Falls back to PR_AF_MODEL when unset # PR_AF_AI_MODEL= diff --git a/Dockerfile b/Dockerfile index 7bfa1a1..fdf1c35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,7 @@ +ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec +FROM ${AFORGE_IMAGE} AS aforge + + FROM python:3.11-slim AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ @@ -14,7 +18,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield==0.1.126" \ + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python" \ "hax-sdk>=0.2.4" \ "pydantic>=2.0" \ "httpx>=0.27" \ @@ -32,7 +36,8 @@ ARG OPENCODE_VERSION=1.17.15 ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ AGENTFIELD_SERVER=http://agentfield:8080 \ - PR_AF_PROVIDER=opencode \ + PR_AF_PROVIDER=aforge \ + AGENTFIELD_AFORGE_COMMAND=exec \ PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 \ PORT=8004 \ HOME=/home/praf \ @@ -55,6 +60,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* COPY --from=builder /install /usr/local +COPY --from=aforge /aforge /usr/local/bin/aforge COPY src/ /app/src/ COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh diff --git a/README.md b/README.md index 0b1c2c1..d02e06b 100644 --- a/README.md +++ b/README.md @@ -263,19 +263,18 @@ The key knobs (see `.env.example` for the full list): |-----------------------------|----------------------------------------------------------------| | `OPENROUTER_API_KEY` | LLM provider key (OpenRouter) — required | | `GH_TOKEN` | GitHub token (`repo` scope) for reading PRs and posting reviews | -| `PR_AF_PROVIDER` | Harness provider (default `opencode`; accepts `aforge`) | +| `PR_AF_PROVIDER` | Harness provider (default `aforge`; use `opencode` to roll back) | +| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command (default `exec`; `do` is explicit) | | `PR_AF_AFORGE_BIN` | Path to an aforge-v2 binary (default `aforge`) | | `PR_AF_HARNESS_BIN` | Provider-agnostic executable override | - -For an unreleased aforge-v2 source benchmark, build `./cmd/aforge`, then run -PR-AF with `PR_AF_PROVIDER=aforge` and either `PR_AF_AFORGE_BIN` (Python node) -or `PR_AF_HARNESS_BIN` (maintained Go node) set to the absolute binary path. -The draft pins the exact AgentField harness commit used for benchmarking; -replace that pin with the released SDK before merging. The current Docker -images do not bundle the unreleased Aforge binary. | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_MAX_COST_USD` | Per-run cost ceiling in USD (default `2.0`) | | `PR_AF_MAX_DURATION_SECONDS`| Per-run wall-clock ceiling in seconds (default `3600`) | + +Both Docker images include AForge and run `exec` by default. The Python node +honors `PR_AF_AFORGE_BIN`; the maintained Go node honors +`PR_AF_HARNESS_BIN`. OpenCode remains installed for a configuration-only +rollback. | `AGENTFIELD_HARNESS_IDLE_SECONDS` | Harness no-output watchdog window in seconds (default `360`) — harness CLIs in JSON mode emit events only at completion boundaries, so long single completions look silent | | `PR_AF_WORKDIR` | Where PR checkouts live (default `/workspaces`); each PR gets its own `-pr` workspace | diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 36e904c..740f87f 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -50,7 +50,10 @@ user_environment: scope: global - name: PR_AF_PROVIDER description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) - default: opencode + default: aforge + - name: AGENTFIELD_AFORGE_COMMAND + description: AForge headless command + default: exec - name: PR_AF_AFORGE_BIN description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) - name: PR_AF_MODEL diff --git a/docker-compose.go.yml b/docker-compose.go.yml index cc93331..35baa56 100644 --- a/docker-compose.go.yml +++ b/docker-compose.go.yml @@ -40,7 +40,8 @@ services: - NODE_ID=pr-af-go - PORT=8007 - AGENT_CALLBACK_URL=http://pr-af-go:8007 - - PR_AF_PROVIDER=${PR_AF_PROVIDER:-opencode} + - PR_AF_PROVIDER=${PR_AF_PROVIDER:-aforge} + - AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec} - PR_AF_MODEL=${PR_AF_MODEL:-openrouter/moonshotai/kimi-k2.5} # Optional generic executable override; leave unset for provider defaults. - PR_AF_HARNESS_BIN diff --git a/docker-compose.yml b/docker-compose.yml index 92ef035..3e4bd78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,8 @@ services: - AGENTFIELD_SERVER=http://agentfield:8080 - AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-} - AGENT_CALLBACK_URL=http://pr-af:8004 - - PR_AF_PROVIDER=${PR_AF_PROVIDER:-opencode} + - PR_AF_PROVIDER=${PR_AF_PROVIDER:-aforge} + - AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec} - PR_AF_MODEL=${PR_AF_MODEL:-openrouter/moonshotai/kimi-k2.5} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} - GH_TOKEN=${GH_TOKEN:-} diff --git a/go/Dockerfile b/go/Dockerfile index deac3d7..44a7959 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -13,8 +13,11 @@ # --------------------------------------------------------------------------- # Stage 1 — builder: fetch modules from the proxy, build the static binary. -# golang 1.23 satisfies go.mod's `go 1.21` directive. +# Go 1.23 satisfies go.mod's `go 1.21` directive. # --------------------------------------------------------------------------- +ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec +FROM ${AFORGE_IMAGE} AS aforge + FROM golang:1.23-bookworm AS builder WORKDIR /src @@ -42,7 +45,8 @@ ARG OPENCODE_VERSION=1.17.15 # PR_AF_HARNESS_BIN remains unset by default; set it only to override every # provider's executable path. ENV AGENTFIELD_SERVER=http://agentfield:8080 \ - PR_AF_PROVIDER=opencode \ + PR_AF_PROVIDER=aforge \ + AGENTFIELD_AFORGE_COMMAND=exec \ PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 \ PORT=8007 \ NODE_ID=pr-af \ @@ -67,6 +71,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* COPY --from=builder /out/pr-af /usr/local/bin/pr-af +COPY --from=aforge /aforge /usr/local/bin/aforge COPY go/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh diff --git a/go/README.md b/go/README.md index 87d8fc9..c258559 100644 --- a/go/README.md +++ b/go/README.md @@ -155,18 +155,15 @@ The node is configured entirely through the environment. | `AGENTFIELD_API_KEY` | Control-plane API key (if the CP has auth enabled) | | `NODE_ID` | Node ID (default `pr-af`) | | `PORT` | Listen port (default `8007`) | -| `PR_AF_PROVIDER` | Harness provider (default `opencode`; accepts `aforge`) | +| `PR_AF_PROVIDER` | Harness provider (default `aforge`; use `opencode` to roll back) | +| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command (default `exec`; `do` is explicit) | | `PR_AF_HARNESS_BIN` | Provider executable override (set to the aforge-v2 binary) | - -To benchmark unreleased aforge-v2 without changing PR-AF prompts, build -`aforge-v2/cmd/aforge`, then set `PR_AF_PROVIDER=aforge` and -`PR_AF_HARNESS_BIN=/absolute/path/to/aforge`. The draft pins the exact -AgentField Go SDK commit used for benchmarking; replace that pin with the -released SDK before merging. The current Docker image does not bundle the -unreleased Aforge binary. | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_LABEL` | Pull-request label that triggers a webhook review (default `pr-af`) | | `PR_AF_MAX_CONCURRENT_REVIEWERS` | Optional webhook review concurrency cap (minimum `1`) | + +The image includes AForge and runs `exec` by default. OpenCode remains +installed and can be selected with `PR_AF_PROVIDER=opencode` without rebuilding. | `PR_AF_MAX_REVIEW_DEPTH` | Optional webhook sub-review depth cap (minimum `0`) | | `PR_AF_MAX_COVERAGE_ITERATIONS` | Optional webhook coverage iteration cap (minimum `1`) | | `PR_AF_HARNESS_BIN` | Optional harness executable override for every provider; unset uses provider defaults | diff --git a/go/agentfield-package.yaml b/go/agentfield-package.yaml index abcadd2..93363bc 100644 --- a/go/agentfield-package.yaml +++ b/go/agentfield-package.yaml @@ -42,7 +42,10 @@ user_environment: scope: global - name: PR_AF_PROVIDER description: harness provider (aforge | claude-code | codex | gemini | opencode) - default: opencode + default: aforge + - name: AGENTFIELD_AFORGE_COMMAND + description: AForge headless command + default: exec - name: PR_AF_MODEL description: harness model default: openrouter/moonshotai/kimi-k2.5 diff --git a/go/cmd/pr-af/main.go b/go/cmd/pr-af/main.go index 7fb8cec..2474060 100644 --- a/go/cmd/pr-af/main.go +++ b/go/cmd/pr-af/main.go @@ -13,7 +13,7 @@ // AGENT_CALLBACK_URL base URL the CP uses to reach this node (else localhost) // NODE_ID node id (default pr-af) // PORT listen port (default 8007) -// PR_AF_PROVIDER harness provider (default opencode; accepts aforge) +// PR_AF_PROVIDER harness provider (default aforge; accepts opencode rollback) // PR_AF_MODEL harness model (env wins over the code default) // PR_AF_HARNESS_BIN optional executable override for every harness provider // OPENROUTER_API_KEY LLM key — required for the .ai() gates; AIConfig is only diff --git a/go/go.mod b/go/go.mod index 8862f99..9a42268 100644 --- a/go/go.mod +++ b/go/go.mod @@ -5,7 +5,7 @@ module github.com/Agent-Field/pr-af/go go 1.21 require ( - github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58 + github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd github.com/golang-jwt/jwt/v5 v5.3.1 github.com/invopop/jsonschema v0.13.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 diff --git a/go/go.sum b/go/go.sum index 826be15..525936c 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,7 @@ github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58 h1:B9ITli2PwuEG+lv7md3CuFAiCnTcuhNIJ6hjNQVTQqQ= github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= +github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd h1:nL0ST2D2Ds7CQhjcs7pbFQdJ/8z+oUXYF9bpQURjx6k= +github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= diff --git a/go/internal/config/ai.go b/go/internal/config/ai.go index 7c53a45..e84a804 100644 --- a/go/internal/config/ai.go +++ b/go/internal/config/ai.go @@ -53,7 +53,7 @@ func AIConfigFromEnv() (AIIntegrationConfig, error) { return AIIntegrationConfig{}, err } return AIIntegrationConfig{ - Provider: strEnv("PR_AF_PROVIDER", "opencode"), + Provider: strEnv("PR_AF_PROVIDER", "aforge"), HarnessModel: strEnv("PR_AF_MODEL", "minimax/minimax-m2.5"), // AI_MODEL falls back to PR_AF_MODEL, then to the code default. AIModel: strEnv("PR_AF_AI_MODEL", strEnv("PR_AF_MODEL", "minimax/minimax-m2.5")), @@ -90,6 +90,7 @@ func (c AIIntegrationConfig) ProviderEnv() map[string]string { } _ = os.MkdirAll(xdg, 0o755) env["XDG_DATA_HOME"] = xdg + env["AGENTFIELD_AFORGE_COMMAND"] = strEnv("AGENTFIELD_AFORGE_COMMAND", "exec") return env } diff --git a/go/internal/config/config_test.go b/go/internal/config/config_test.go index fbbbe99..80afbfe 100644 --- a/go/internal/config/config_test.go +++ b/go/internal/config/config_test.go @@ -134,8 +134,8 @@ func TestResolveBudgetCapsCascade(t *testing.T) { func TestAIConfigFromEnvDefaults(t *testing.T) { clearConfigEnv(t) c := mustAIConfig(t) - if c.Provider != "opencode" { - t.Errorf("Provider = %q, want opencode", c.Provider) + if c.Provider != "aforge" { + t.Errorf("Provider = %q, want aforge", c.Provider) } // The CODE default is minimax — NOT the manifest's kimi default. env wins, // but with no env this must be minimax (design §B.6). @@ -238,6 +238,9 @@ func TestProviderEnv(t *testing.T) { if env["XDG_DATA_HOME"] != xdg { t.Errorf("XDG_DATA_HOME = %q, want %q", env["XDG_DATA_HOME"], xdg) } + if env["AGENTFIELD_AFORGE_COMMAND"] != "exec" { + t.Errorf("AGENTFIELD_AFORGE_COMMAND = %q, want exec", env["AGENTFIELD_AFORGE_COMMAND"]) + } // With XDG_DATA_HOME unset, ProviderEnv falls back to a tmp dir and creates // it. diff --git a/go/internal/harnessx/run.go b/go/internal/harnessx/run.go index fd8c911..6a25354 100644 --- a/go/internal/harnessx/run.go +++ b/go/internal/harnessx/run.go @@ -80,7 +80,7 @@ func seedDefaults[T any]() T { // router.harness (system_prompt, schema, model, provider, tools, cwd, max_turns, // permission_mode). type RoleOptions struct { - // Provider is the harness ADAPTER string, e.g. "aforge" or "opencode" (PR-AF's default), + // Provider is the harness ADAPTER string, e.g. "aforge" (PR-AF's default) or "opencode", // "claude-code", "codex". Provider string diff --git a/go/internal/node/node.go b/go/internal/node/node.go index d4dcdc7..a88ff92 100644 --- a/go/internal/node/node.go +++ b/go/internal/node/node.go @@ -139,7 +139,7 @@ func resolvedHarnessBin(c config.AIIntegrationConfig) string { // - PORT default "8007" -> ListenAddress ":8007". // - AGENT_CALLBACK_URL -> Config.PublicURL — the base URL the CP uses to reach // this node; unset falls back to the SDK's http://localhost:. -// - HarnessConfig / AIConfig — the harness (opencode) + LLM credentials the +// - HarnessConfig / AIConfig — the selected harness + LLM credentials the // reasoners rely on. Every reasoner calls the harness with only Cwd set, so // the agent's default HarnessConfig Provider/Model must be present, and the // two .ai() gates (intake/coverage) need AIConfig. Mirrors app.py's diff --git a/go/internal/node/node_test.go b/go/internal/node/node_test.go index 9011475..83aaa6b 100644 --- a/go/internal/node/node_test.go +++ b/go/internal/node/node_test.go @@ -45,7 +45,11 @@ func TestHarnessConfigPreservesExistingFields(t *testing.T) { if got.Provider != conf.Provider || got.Model != conf.HarnessModel || got.MaxTurns != conf.MaxTurns || got.PermissionMode != "auto" || got.BinPath != conf.OpencodeBin { t.Errorf("harnessConfig fields = %+v", got) } - if want := map[string]string{"OPENAI_API_KEY": "openai-key", "XDG_DATA_HOME": xdg}; !reflect.DeepEqual(got.Env, want) { + if want := map[string]string{ + "OPENAI_API_KEY": "openai-key", + "XDG_DATA_HOME": xdg, + "AGENTFIELD_AFORGE_COMMAND": "exec", + }; !reflect.DeepEqual(got.Env, want) { t.Errorf("Env = %#v, want %#v", got.Env, want) } } diff --git a/go/test/functional/compose.functional.yml b/go/test/functional/compose.functional.yml index 2b82a13..f191163 100644 --- a/go/test/functional/compose.functional.yml +++ b/go/test/functional/compose.functional.yml @@ -49,7 +49,8 @@ services: - NODE_ID=pr-af - PORT=8007 - AGENT_CALLBACK_URL=http://pr-af:8007 - - PR_AF_PROVIDER=${PR_AF_PROVIDER:-opencode} + - PR_AF_PROVIDER=${PR_AF_PROVIDER:-aforge} + - AGENTFIELD_AFORGE_COMMAND=${AGENTFIELD_AFORGE_COMMAND:-exec} - PR_AF_MODEL=${PR_AF_MODEL:-openrouter/moonshotai/kimi-k2.5} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} - GH_TOKEN=${GH_TOKEN:-} diff --git a/pyproject.toml b/pyproject.toml index 4df6134..f8e1ee7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@72f3d00baf58efc4fec3f7ee16e69c7cb8f99ff9#subdirectory=sdk/python", + "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python", "hax-sdk>=0.2.4", "pydantic>=2.0", "httpx>=0.27", diff --git a/src/pr_af/config.py b/src/pr_af/config.py index df74e1c..87bbd35 100644 --- a/src/pr_af/config.py +++ b/src/pr_af/config.py @@ -313,7 +313,7 @@ def from_yaml(cls, path: str) -> ReviewConfig: class AIIntegrationConfig(BaseModel): - provider: str = Field(default_factory=lambda: os.getenv("PR_AF_PROVIDER", "opencode")) + provider: str = Field(default_factory=lambda: os.getenv("PR_AF_PROVIDER", "aforge")) harness_model: str = Field( default_factory=lambda: os.getenv("PR_AF_MODEL", "minimax/minimax-m2.5") ) @@ -352,6 +352,7 @@ def provider_env(self) -> dict[str, str]: "GH_TOKEN", ) env: dict[str, str] = {key: value for key in env_keys if (value := os.getenv(key))} + env["AGENTFIELD_AFORGE_COMMAND"] = os.getenv("AGENTFIELD_AFORGE_COMMAND", "exec") xdg = os.getenv("XDG_DATA_HOME") or os.path.join(tempfile.gettempdir(), "opencode-shared-data") os.makedirs(xdg, exist_ok=True) env["XDG_DATA_HOME"] = xdg diff --git a/tests/test_aforge_config.py b/tests/test_aforge_config.py index 6918c26..ae0fde4 100644 --- a/tests/test_aforge_config.py +++ b/tests/test_aforge_config.py @@ -20,3 +20,19 @@ def test_generic_harness_binary_is_available_to_all_providers(monkeypatch) -> No config = AIIntegrationConfig.from_env() assert config.harness_bin == "/opt/harness" + + +def test_aforge_exec_is_the_default(monkeypatch) -> None: + monkeypatch.delenv("PR_AF_PROVIDER", raising=False) + monkeypatch.delenv("AGENTFIELD_AFORGE_COMMAND", raising=False) + + config = AIIntegrationConfig.from_env() + + assert config.provider == "aforge" + assert config.provider_env()["AGENTFIELD_AFORGE_COMMAND"] == "exec" + + +def test_opencode_remains_an_explicit_rollback(monkeypatch) -> None: + monkeypatch.setenv("PR_AF_PROVIDER", "opencode") + + assert AIIntegrationConfig.from_env().provider == "opencode" From 5cdcd7a951102c0ed1bb4c7d48a899d7433bcc67 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:54:09 -0400 Subject: [PATCH 03/10] build: fetch the released AForge CLI instead of pulling a private image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both images pulled the AForge binary out of ghcr.io/agent-field/aforge-v2:chat-v2-exec. That package is private, so `docker build` failed with a 403 for anyone without registry access — including CI. Replace the image stage in the root (Python) and go/ Dockerfiles with a fetch stage that downloads the published, gzipped release asset over HTTPS, verifies the decompressed binary against the release checksums.txt, and chmod +x's it. AFORGE_BASE_URL and AFORGE_VERSION are build args, so CI or a local mirror can point the fetch elsewhere without editing the Dockerfile. Both runtime stages already install ca-certificates, which AForge needs to reach the model provider over HTTPS. Co-Authored-By: Claude Fable 5 --- Dockerfile | 35 ++++++++++++++++++++++++++++++++--- go/Dockerfile | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index fdf1c35..26fed69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,34 @@ -ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec -FROM ${AFORGE_IMAGE} AS aforge +# --------------------------------------------------------------------------- +# Stage 0 — aforge: fetch the released AForge CLI from the public download host +# and verify it against the release checksums (which hash the DECOMPRESSED +# binaries). Both ARGs are overridable so CI or a local mirror can serve the +# assets from somewhere else: +# +# docker build --build-arg AFORGE_BASE_URL=... --build-arg AFORGE_VERSION=... . +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim AS aforge + +ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge +ARG AFORGE_VERSION=build-9b3ff482de3f +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /out +RUN set -eux; \ + arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${arch}.gz" -o aforge.gz; \ + gunzip -c aforge.gz > aforge; \ + rm -f aforge.gz; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/checksums.txt" -o checksums.txt; \ + grep " aforge-linux-${arch}$" checksums.txt | sed 's/ aforge-linux-.*/ aforge/' > aforge.sha256; \ + test -s aforge.sha256; \ + sha256sum -c aforge.sha256; \ + rm -f checksums.txt aforge.sha256; \ + chmod +x aforge FROM python:3.11-slim AS builder @@ -60,7 +89,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* COPY --from=builder /install /usr/local -COPY --from=aforge /aforge /usr/local/bin/aforge +COPY --from=aforge /out/aforge /usr/local/bin/aforge COPY src/ /app/src/ COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh diff --git a/go/Dockerfile b/go/Dockerfile index 44a7959..3133d87 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -11,13 +11,43 @@ # GOWORK=off, and no `replace` dance. `go mod download` pulls the SDK and every # other dependency straight from the module proxy, cache-keyed on go.mod/go.sum. +# --------------------------------------------------------------------------- +# Stage 0 — aforge: fetch the released AForge CLI from the public download host +# and verify it against the release checksums (which hash the DECOMPRESSED +# binaries). Both ARGs are overridable so CI or a local mirror can serve the +# assets from somewhere else: +# +# docker build --build-arg AFORGE_BASE_URL=... --build-arg AFORGE_VERSION=... . +# --------------------------------------------------------------------------- +FROM debian:bookworm-slim AS aforge + +ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge +ARG AFORGE_VERSION=build-9b3ff482de3f +ARG TARGETARCH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /out +RUN set -eux; \ + arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/aforge-linux-${arch}.gz" -o aforge.gz; \ + gunzip -c aforge.gz > aforge; \ + rm -f aforge.gz; \ + curl -fsSL "${AFORGE_BASE_URL}/${AFORGE_VERSION}/checksums.txt" -o checksums.txt; \ + grep " aforge-linux-${arch}$" checksums.txt | sed 's/ aforge-linux-.*/ aforge/' > aforge.sha256; \ + test -s aforge.sha256; \ + sha256sum -c aforge.sha256; \ + rm -f checksums.txt aforge.sha256; \ + chmod +x aforge + + # --------------------------------------------------------------------------- # Stage 1 — builder: fetch modules from the proxy, build the static binary. # Go 1.23 satisfies go.mod's `go 1.21` directive. # --------------------------------------------------------------------------- -ARG AFORGE_IMAGE=ghcr.io/agent-field/aforge-v2:chat-v2-exec -FROM ${AFORGE_IMAGE} AS aforge - FROM golang:1.23-bookworm AS builder WORKDIR /src @@ -71,7 +101,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ rm -rf /var/lib/apt/lists/* COPY --from=builder /out/pr-af /usr/local/bin/pr-af -COPY --from=aforge /aforge /usr/local/bin/aforge +COPY --from=aforge /out/aforge /usr/local/bin/aforge COPY go/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh From 274255fcd797a3707eadc88598e4e2f0e836917c Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:54:15 -0400 Subject: [PATCH 04/10] build: pin the Python node to agentfield 0.1.129 from PyPI The branch pinned the SDK to a git commit on the adapter branch, which also forced hatchling's allow-direct-references escape hatch on. The AForge provider has shipped on PyPI since 0.1.127, so a normal version floor is enough: 0.1.129 registers the "aforge" provider and honours HarnessConfig.aforge_bin, which is all src/pr_af/app.py passes. Verified against the installed wheel: harness.providers._factory SUPPORTED_PROVIDERS contains "aforge" and builds AforgeProvider from config.aforge_bin. Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 +- pyproject.toml | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 26fed69..78a8cff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python" \ + "agentfield>=0.1.129" \ "hax-sdk>=0.2.4" \ "pydantic>=2.0" \ "httpx>=0.27" \ diff --git a/pyproject.toml b/pyproject.toml index f8e1ee7..836963d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield @ git+https://github.com/Agent-Field/agentfield.git@bfd34426d1bdec3cbbfa946682a22ef0a1b91503#subdirectory=sdk/python", + "agentfield>=0.1.129", "hax-sdk>=0.2.4", "pydantic>=2.0", "httpx>=0.27", @@ -36,9 +36,6 @@ pr-af = "pr_af.app:main" [tool.hatch.build.targets.wheel] packages = ["src/pr_af"] -[tool.hatch.metadata] -allow-direct-references = true - [tool.ruff] target-version = "py311" line-length = 120 From f7a985d5fffd50d06aaa7784d0b59cdcb73227ce Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:54:22 -0400 Subject: [PATCH 05/10] chore(go): drop the superseded sdk/go go.sum entry The branch bumped the sdk/go require but left the previous v0.1.127-rc.5.0.20260810165835-72f3d00baf58 hashes behind, so go.sum carried two versions of a module that only one require references. `go mod tidy` removes exactly those two lines; go.mod is unchanged. Co-Authored-By: Claude Fable 5 --- go/go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go/go.sum b/go/go.sum index 525936c..9f62ae7 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,3 @@ -github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58 h1:B9ITli2PwuEG+lv7md3CuFAiCnTcuhNIJ6hjNQVTQqQ= -github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260810165835-72f3d00baf58/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd h1:nL0ST2D2Ds7CQhjcs7pbFQdJ/8z+oUXYF9bpQURjx6k= github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= From 45437001f949d9173f4a48fd5b4ca899e5c1677b Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 10:54:32 -0400 Subject: [PATCH 06/10] docs: repair the env-var tables and describe the AForge fetch The branch dropped a prose paragraph into the middle of the env-var table in both READMEs, which truncates the rendered table at that point and leaves the remaining rows as literal pipe text. Move the prose below the table in each file, and drop the duplicate PR_AF_HARNESS_BIN row the branch added to go/README.md (the table already documented it further down). Also state where the AForge binary comes from now (checksum-verified release asset, pinned by AFORGE_VERSION), and scope AGENTFIELD_AFORGE_COMMAND honestly: the Go SDK adapter reads it, the Python SDK on PyPI always runs `exec`. Co-Authored-By: Claude Fable 5 --- .env.example | 1 + README.md | 17 +++++++++++------ agentfield-package.yaml | 2 +- go/README.md | 13 +++++++------ go/agentfield-package.yaml | 2 +- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index dc933af..91eb736 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ NODE_ID=pr-af # --- AI / harness config --- # AForge exec is the default. Set PR_AF_PROVIDER=opencode to roll back. PR_AF_PROVIDER=aforge +# Read by the Go node's SDK adapter; the pinned Python SDK always runs `exec`. AGENTFIELD_AFORGE_COMMAND=exec PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 # Falls back to PR_AF_MODEL when unset diff --git a/README.md b/README.md index d02e06b..5876434 100644 --- a/README.md +++ b/README.md @@ -264,20 +264,25 @@ The key knobs (see `.env.example` for the full list): | `OPENROUTER_API_KEY` | LLM provider key (OpenRouter) — required | | `GH_TOKEN` | GitHub token (`repo` scope) for reading PRs and posting reviews | | `PR_AF_PROVIDER` | Harness provider (default `aforge`; use `opencode` to roll back) | -| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command (default `exec`; `do` is explicit) | +| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command (default `exec`) — read by the Go node's SDK adapter; the pinned Python SDK always runs `exec` | | `PR_AF_AFORGE_BIN` | Path to an aforge-v2 binary (default `aforge`) | | `PR_AF_HARNESS_BIN` | Provider-agnostic executable override | | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_MAX_COST_USD` | Per-run cost ceiling in USD (default `2.0`) | | `PR_AF_MAX_DURATION_SECONDS`| Per-run wall-clock ceiling in seconds (default `3600`) | - -Both Docker images include AForge and run `exec` by default. The Python node -honors `PR_AF_AFORGE_BIN`; the maintained Go node honors -`PR_AF_HARNESS_BIN`. OpenCode remains installed for a configuration-only -rollback. | `AGENTFIELD_HARNESS_IDLE_SECONDS` | Harness no-output watchdog window in seconds (default `360`) — harness CLIs in JSON mode emit events only at completion boundaries, so long single completions look silent | | `PR_AF_WORKDIR` | Where PR checkouts live (default `/workspaces`); each PR gets its own `-pr` workspace | +Both Docker images ship the released AForge CLI (fetched and checksum-verified +at build time from `https://agentfield.ai/downloads/aforge`) and run `exec` by +default. The Python node resolves the binary from `PR_AF_AFORGE_BIN` (or +`PR_AF_HARNESS_BIN`); the maintained Go node resolves it from +`PR_AF_HARNESS_BIN`. OpenCode stays installed in both images, so +`PR_AF_PROVIDER=opencode` is a configuration-only rollback — no rebuild. + +The image's AForge version is pinned by the `AFORGE_VERSION` build arg; +`AFORGE_BASE_URL` points the fetch at a different host when needed. + ## GitHub Actions Integration The easiest way to use PR-AF is to drop it into your GitHub Actions. It requires **zero configuration** and runs securely using GitHub's built-in `GITHUB_TOKEN`. diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 740f87f..cacc93a 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -52,7 +52,7 @@ user_environment: description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: aforge - name: AGENTFIELD_AFORGE_COMMAND - description: AForge headless command + description: AForge headless command (exec | do); the pinned Python SDK always runs exec default: exec - name: PR_AF_AFORGE_BIN description: Optional path to the aforge-v2 binary (defaults to aforge on PATH) diff --git a/go/README.md b/go/README.md index c258559..3791626 100644 --- a/go/README.md +++ b/go/README.md @@ -156,22 +156,23 @@ The node is configured entirely through the environment. | `NODE_ID` | Node ID (default `pr-af`) | | `PORT` | Listen port (default `8007`) | | `PR_AF_PROVIDER` | Harness provider (default `aforge`; use `opencode` to roll back) | -| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command (default `exec`; `do` is explicit) | -| `PR_AF_HARNESS_BIN` | Provider executable override (set to the aforge-v2 binary) | +| `AGENTFIELD_AFORGE_COMMAND` | AForge headless command — `exec` (default) or `do` | | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | | `PR_AF_LABEL` | Pull-request label that triggers a webhook review (default `pr-af`) | | `PR_AF_MAX_CONCURRENT_REVIEWERS` | Optional webhook review concurrency cap (minimum `1`) | - -The image includes AForge and runs `exec` by default. OpenCode remains -installed and can be selected with `PR_AF_PROVIDER=opencode` without rebuilding. | `PR_AF_MAX_REVIEW_DEPTH` | Optional webhook sub-review depth cap (minimum `0`) | | `PR_AF_MAX_COVERAGE_ITERATIONS` | Optional webhook coverage iteration cap (minimum `1`) | -| `PR_AF_HARNESS_BIN` | Optional harness executable override for every provider; unset uses provider defaults | +| `PR_AF_HARNESS_BIN` | Optional harness executable override for every provider (point it at an `aforge` binary outside `PATH`); unset uses provider defaults | | `PR_AF_MAX_COST_USD` | Per-run cost ceiling in USD (default `2.0`) | | `PR_AF_MAX_DURATION_SECONDS`| Per-run wall-clock ceiling in seconds (default `3600`) | | `AGENTFIELD_HARNESS_IDLE_SECONDS` | Harness no-output watchdog window in seconds (default `360`) — harness CLIs in JSON mode emit events only at completion boundaries, so long single completions look silent | | `HAX_API_KEY` | Optional — enables the HITL review-approval gate when set | +The image ships the released AForge CLI (fetched and checksum-verified at build +time from `https://agentfield.ai/downloads/aforge`, pinned by the +`AFORGE_VERSION` build arg) and runs `exec` by default. OpenCode remains +installed and can be selected with `PR_AF_PROVIDER=opencode` without rebuilding. + Note: the code default model is `minimax/minimax-m2.5`, while the Docker image / compose / manifest set `PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5`. The env var always wins; both defaults are intentional (they mirror the Python node). diff --git a/go/agentfield-package.yaml b/go/agentfield-package.yaml index 93363bc..5e2d7b3 100644 --- a/go/agentfield-package.yaml +++ b/go/agentfield-package.yaml @@ -44,7 +44,7 @@ user_environment: description: harness provider (aforge | claude-code | codex | gemini | opencode) default: aforge - name: AGENTFIELD_AFORGE_COMMAND - description: AForge headless command + description: AForge headless command (exec | do) default: exec - name: PR_AF_MODEL description: harness model From 83d4d92b73d78691b97e43ae7a59a51ad44bd5d3 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 13:10:07 -0400 Subject: [PATCH 07/10] chore: pin aforge to v0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aforge-v2 cut its first semver release (v0.1.0), so the AFORGE_VERSION default moves off the build- coordinate onto the tag. Bumping the string is what busts the fetch layer's cache, so this is what actually pulls the released binary instead of restoring the stale one. Both the Python and the Go image fetch the same coordinate, so both ARG defaults move together. The AgentField SDK pins are deliberately left alone — they bump on their own release. Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 +- go/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78a8cff..590ced9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ FROM debian:bookworm-slim AS aforge ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge -ARG AFORGE_VERSION=build-9b3ff482de3f +ARG AFORGE_VERSION=v0.1.0 ARG TARGETARCH RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/go/Dockerfile b/go/Dockerfile index 3133d87..a4f9e29 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -22,7 +22,7 @@ FROM debian:bookworm-slim AS aforge ARG AFORGE_BASE_URL=https://agentfield.ai/downloads/aforge -ARG AFORGE_VERSION=build-9b3ff482de3f +ARG AFORGE_VERSION=v0.1.0 ARG TARGETARCH RUN apt-get update && apt-get install -y --no-install-recommends \ From febcc6d6a51011f55791cf870e58c608ff349bac Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 21:43:05 -0400 Subject: [PATCH 08/10] chore: pin agentfield 0.1.130 (Python >=0.1.130, Go SDK sdk/go/v0.1.130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.1.130 is the release that ships the AForge-default harness resolution both nodes rely on, so both pins move to it: - Python (pyproject.toml, Dockerfile): agentfield>=0.1.129 -> >=0.1.130. - Go (go/go.mod, go/go.sum): replace the v0.1.127-rc.5 pseudo-version — whose commit is not reachable from any branch — with the released module tag sdk/go/v0.1.130. go build/vet/test and gofmt are clean against the released module. Co-Authored-By: Claude Fable 5 --- Dockerfile | 2 +- go/go.mod | 2 +- go/go.sum | 4 ++-- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 590ced9..f497dcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ COPY pyproject.toml README.md ./ COPY src/ src/ RUN pip install --no-cache-dir --prefix=/install \ - "agentfield>=0.1.129" \ + "agentfield>=0.1.130" \ "hax-sdk>=0.2.4" \ "pydantic>=2.0" \ "httpx>=0.27" \ diff --git a/go/go.mod b/go/go.mod index 9a42268..2ed05e1 100644 --- a/go/go.mod +++ b/go/go.mod @@ -5,7 +5,7 @@ module github.com/Agent-Field/pr-af/go go 1.21 require ( - github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd + github.com/Agent-Field/agentfield/sdk/go v0.1.130 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/invopop/jsonschema v0.13.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 diff --git a/go/go.sum b/go/go.sum index 9f62ae7..f99fd26 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,5 @@ -github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd h1:nL0ST2D2Ds7CQhjcs7pbFQdJ/8z+oUXYF9bpQURjx6k= -github.com/Agent-Field/agentfield/sdk/go v0.1.127-rc.5.0.20260815031530-bfd34426d1bd/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= +github.com/Agent-Field/agentfield/sdk/go v0.1.130 h1:k6ATecElqx54AUGzmFnbJy/BrFY+2UhPV7VM3X8ByTw= +github.com/Agent-Field/agentfield/sdk/go v0.1.130/go.mod h1:08VZk14uw4GJH6a34psHkuLu+DcRr197Zi0IGmLlfrM= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= diff --git a/pyproject.toml b/pyproject.toml index 836963d..10d02be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" requires-python = ">=3.11" authors = [{ name = "AgentField", email = "hello@agentfield.dev" }] dependencies = [ - "agentfield>=0.1.129", + "agentfield>=0.1.130", "hax-sdk>=0.2.4", "pydantic>=2.0", "httpx>=0.27", From 0bd2a40605973d1b738e3d2d5d215782609880bb Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 22:26:19 -0400 Subject: [PATCH 09/10] fix(go/docker): set WORKDIR /workspaces so cwd-less harness calls can write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime stage runs as praf but never set a WORKDIR, so the process cwd was root-owned `/`. The Go SDK creates its schema output dir under the cwd when a harness call carries no Cwd — planning, coverage, compound dedup, worthiness and the intake fallback all do — and every such call failed with `creating temp output dir: mkdir ./.agentfield-out-…: permission denied`, after the review had already spent its LLM budget. /workspaces is praf-owned and already PR_AF_WORKDIR. Co-Authored-By: Claude Fable 5 --- go/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/go/Dockerfile b/go/Dockerfile index a4f9e29..4be6bf4 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -106,6 +106,11 @@ COPY go/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh USER praf +# Cwd must be writable by praf: the AgentField Go SDK creates its schema +# output dir under the process cwd when a harness call carries no Cwd +# (planning/coverage/dedup/worthiness/intake-fallback do), and `/` is +# root-owned. /workspaces is praf-owned and is PR_AF_WORKDIR already. +WORKDIR /workspaces EXPOSE 8007 From aac55e2957ccf3038d0d81ff1a6f4521cccb1562 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Mon, 17 Aug 2026 22:26:19 -0400 Subject: [PATCH 10/10] docs(manifest): AGENTFIELD_AFORGE_COMMAND is a live exec|do selector on 0.1.130 Co-Authored-By: Claude Fable 5 --- agentfield-package.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentfield-package.yaml b/agentfield-package.yaml index cacc93a..d838577 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -52,7 +52,7 @@ user_environment: description: Coding-agent harness provider (aforge | claude-code | codex | gemini | opencode) default: aforge - name: AGENTFIELD_AFORGE_COMMAND - description: AForge headless command (exec | do); the pinned Python SDK always runs exec + description: AForge headless command the SDK runs (exec | do); default exec on agentfield>=0.1.130 default: exec - name: PR_AF_AFORGE_BIN description: Optional path to the aforge-v2 binary (defaults to aforge on PATH)