Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b6d2ca3
feat(go): module scaffold + reusable infra (afx, fatal, harnessx)
AbirAbbas Jul 10, 2026
6e1da51
feat(go): deployment artifacts for the opt-in Go sibling
AbirAbbas Jul 10, 2026
eb246a0
feat(go): schemas and config packages
AbirAbbas Jul 10, 2026
637b688
feat(go): hitl substrate — hax REST client and Pauser seam
AbirAbbas Jul 10, 2026
2bf218d
feat(go): scoring engine with divergent severity map and Python rounding
AbirAbbas Jul 10, 2026
0f00a39
feat(go): diffengine — unified-diff parser, stats, clustering
AbirAbbas Jul 10, 2026
5fcdfdd
feat(go): GitHub REST client with PAT and App-JWT auth
AbirAbbas Jul 10, 2026
22a3261
feat(go): evidence extraction and blast-radius packages
AbirAbbas Jul 10, 2026
223f77d
feat(go): byte-verbatim prompt builders with committed golden generator
AbirAbbas Jul 10, 2026
f666925
feat(go): merge gate and polish passes
AbirAbbas Jul 10, 2026
f1b00ad
feat(go): HITL review gate
AbirAbbas Jul 10, 2026
5b0f9cb
feat(go): the 16 in-process reasoner functions
AbirAbbas Jul 10, 2026
7d536d8
feat(go): review orchestrator
AbirAbbas Jul 10, 2026
a43882f
feat(go): node wiring, 17-reasoner registration, GitHub webhook, main
AbirAbbas Jul 10, 2026
a42f60e
test(go): functional suite and zero-LLM e2e harness
AbirAbbas Jul 10, 2026
db92989
test(go): make the e2e mock schema-valid and retry-aware
AbirAbbas Jul 10, 2026
b35c96d
fix(go): address review findings — env parsing, executionId, make con…
AbirAbbas Jul 10, 2026
4b5f828
fix(go): harness schemas now match pydantic, not invopop reflection
AbirAbbas Jul 10, 2026
69cf522
chore(go): bump SDK pin to merged agentfield v0.1.108
AbirAbbas Jul 13, 2026
04222a2
fix(go): strip LiteLLM's openrouter/ routing prefix on the .ai() model
AbirAbbas Jul 13, 2026
9a17cbf
fix(go): mirror the Python SDK's structured .ai() parse tolerance
AbirAbbas Jul 13, 2026
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,24 @@ jobs:
- uses: actions/checkout@v4
- name: Build Docker Image
run: docker build -t pr-af:test .

go:
runs-on: ubuntu-latest
defaults:
run:
working-directory: go
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.21"
cache-dependency-path: go/go.sum
- name: Build
run: go build ./...
- name: Vet
run: go vet ./...
- name: Test
run: go test ./...
- name: Gofmt
run: test -z "$(gofmt -l .)"
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,15 @@ When the writer and the reviewer are the same intelligence, the pull request gat
</p>

[Read the post →](https://www.agentfield.ai/blog/ai-native-code-review?utm_source=github-readme&utm_campaign=pr-af-readme&utm_id=pr-af-readme-blog-ai-native-code-review)

---

## Go implementation (opt-in)

This repo also ships a Go port of the node under [`go/`](go/README.md). The
**Python implementation is the default** — everything above is unchanged and
still runs as `pr-af` (`:8004`). The Go port registers **separately** as
`pr-af-go` (`:8007`), so both can run against one control plane simultaneously.
Opt in by targeting the `-go` reasoner path, e.g.
`POST /api/v1/execute/async/pr-af-go.review`. Build, run, and Docker/compose
docs live in [`go/README.md`](go/README.md).
76 changes: 76 additions & 0 deletions docker-compose.go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# PR-AF Go node — opt-in ADD-ON to the Python stack.
#
# The Python docker-compose.yml is the DEFAULT stack (the AgentField control
# plane `agentfield` + the Python `pr-af` node on :8004) and is left 100%
# untouched. This file adds ONLY the Go node, registered under a DISTINCT
# identity so both nodes can run against one control plane simultaneously:
#
# pr-af-go -> node id "pr-af-go", :8007
#
# Run story (two commands, Python stack first):
#
# docker compose up -d # Python stack + control plane
# docker compose -f docker-compose.go.yml up -d # adds the Go node
#
# This is a SEPARATE compose project (name: pr-af-go) that joins the Python
# stack's network as an EXTERNAL reference, so AGENTFIELD_SERVER=
# http://agentfield:8080 resolves and the Go node shares the Python stack's
# workspaces volume over that network. The control plane (service `agentfield`)
# lives in the Python project, so there is NO `depends_on` here — bring the
# Python stack up first.
#
# COMPOSE_PROJECT_NAME caveat: the external network/volume names below
# (pr-af_default, pr-af_pr-af-workspaces) are the Python project's
# default-project-name resources. The Python docker-compose.yml has NO explicit
# `name:`, so its project name defaults to the compose directory's basename —
# `pr-af` when the repo is checked out as a directory named `pr-af`. If you set
# COMPOSE_PROJECT_NAME for the Python stack (or the checkout directory is named
# something else), override the external `name:` fields below to match
# `<project>_default` and `<project>_pr-af-workspaces`.
name: pr-af-go

services:
pr-af-go:
build:
context: .
dockerfile: go/Dockerfile
environment:
- AGENTFIELD_SERVER=http://agentfield:8080 # CP service name in pr-af's compose is "agentfield"
- AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-}
- NODE_ID=pr-af-go
- PORT=8007
- AGENT_CALLBACK_URL=http://pr-af-go:8007
- PR_AF_PROVIDER=${PR_AF_PROVIDER:-opencode}
- PR_AF_MODEL=${PR_AF_MODEL:-openrouter/moonshotai/kimi-k2.5}
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
- GH_TOKEN=${GH_TOKEN:-}
- XDG_DATA_HOME=/home/praf/.local/share
- PR_AF_WORKDIR=/workspaces
ports:
- "8007:8007"
volumes:
- workspaces:/workspaces
- opencode-data:/home/praf/.local/share
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8007/health"]
interval: 60s
timeout: 30s
retries: 5
start_period: 30s
restart: unless-stopped

# Join the Python stack's default network so `agentfield` (the control plane)
# resolves by service name. external => Compose does NOT create it; the Python
# stack must be up first (see COMPOSE_PROJECT_NAME caveat in the header).
networks:
default:
external: true
name: pr-af_default

# Share the Python stack's workspaces volume (<project>_<volume> =
# pr-af_pr-af-workspaces); keep opencode data node-local.
volumes:
workspaces:
external: true
name: pr-af_pr-af-workspaces
opencode-data: {}
11 changes: 11 additions & 0 deletions go/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Local Go workspace used during SDK-parity development to resolve the
# agentfield Go SDK to a local worktree. Must NOT be committed — CI/Docker rely
# on the real versioned require in go.mod (no replace directive) instead.
go.work
go.work.sum

# Compiled binaries.
bin/

# stray single-package build artifact (go build ./test/mockcli)
/mockcli
79 changes: 79 additions & 0 deletions go/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# PR-AF Go node — multi-stage build.
#
# Build from the PR-AF repo ROOT so the go/ module is in the build context and
# the paths below (go/go.mod, go/) resolve. The docker-compose.go.yml add-on
# builds it exactly this way (build.context: ., dockerfile: go/Dockerfile):
#
# docker build -f go/Dockerfile -t pr-af-go:latest .
#
# Unlike the SWE-AF Go port, the AgentField Go SDK is a REAL versioned require
# resolved from proxy.golang.org — so there is NO sparse SDK clone stage, no
# 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 1 — builder: fetch modules from the proxy, build the static binary.
# golang 1.23 satisfies go.mod's `go 1.21` directive.
# ---------------------------------------------------------------------------
FROM golang:1.23-bookworm AS builder

WORKDIR /src

# Prime the module cache from go.mod/go.sum first so dependency downloads cache
# independently of source edits (this layer re-runs only when go.mod/go.sum change).
COPY go/go.mod go/go.sum ./
RUN go mod download

# Copy the rest of the module and build a static binary.
COPY go/ ./
ENV CGO_ENABLED=0 GOOS=linux
RUN go build -trimpath -ldflags="-s -w" -o /out/pr-af ./cmd/pr-af

# ---------------------------------------------------------------------------
# Stage 2 — runtime: slim Debian mirroring the Python image (opencode CLI + a
# non-root praf user), but shipping the single static Go binary instead of a
# Python runtime. The entrypoint is byte-identical to the Python one — it
# generates opencode.json from PR_AF_MODEL at container start.
# ---------------------------------------------------------------------------
FROM debian:bookworm-slim AS runtime

ARG OPENCODE_VERSION=1.17.15

ENV AGENTFIELD_SERVER=http://agentfield:8080 \
PR_AF_PROVIDER=opencode \
PR_AF_MODEL=openrouter/moonshotai/kimi-k2.5 \
PORT=8007 \
NODE_ID=pr-af-go \
HOME=/home/praf \
PATH=/home/praf/.opencode/bin:${PATH} \
XDG_DATA_HOME=/home/praf/.local/share \
PR_AF_WORKDIR=/workspaces

# System deps: ca-certificates (HTTPS to the LLM provider + GitHub), curl
# (healthcheck + opencode installer), git (repo_path reviews clone/fetch/checkout).
# Create the non-root praf user (uid/gid 10001) and install the opencode CLI as
# that user so it lands under /home/praf/.opencode (on PATH above).
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git && \
groupadd --gid 10001 praf && \
useradd --uid 10001 --gid praf --create-home --home-dir /home/praf --shell /bin/sh praf && \
su -s /bin/sh praf -c "curl -fsSL https://opencode.ai/install | bash -s -- --version ${OPENCODE_VERSION} --no-modify-path" && \
mkdir -p /workspaces /home/praf/.local/share && \
chown -R praf:praf /workspaces /home/praf && \
rm -rf /var/lib/apt/lists/*

COPY --from=builder /out/pr-af /usr/local/bin/pr-af
COPY go/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

USER praf

EXPOSE 8007

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8007/health || exit 1

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["/usr/local/bin/pr-af"]
61 changes: 61 additions & 0 deletions go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# PR-AF Go port — build/test targets. Run from the go/ directory.
# Mirrors the acceptance gate used by every port task and CI (design §F, §G):
# go build ./... && go vet ./... && go test ./...
#
# The AgentField Go SDK is a REAL versioned require resolved from
# proxy.golang.org (no replace directive), so no GOWORK/replace dance is needed
# for CI/Docker. A gitignored go.work is the local dev path; set GOWORK=off to
# ignore it explicitly.

.PHONY: build vet test check fmt lint run docker-build docker-up docker-down

# Compile every package in the module.
build:
go build ./...

# Static analysis (go vet) across the module.
vet:
go vet ./...

# Unit tests across the module.
test:
go test ./...

# The full local gate CI runs: build + vet + tests.
check: build vet test

# Format every Go source file in place.
fmt:
gofmt -w .

# Optional lint pass; no-op with a hint when golangci-lint is not installed.
lint:
@command -v golangci-lint >/dev/null 2>&1 && golangci-lint run || \
echo "golangci-lint not installed; skipping (install: https://golangci-lint.run)"

# Run the PR-AF node (pr-af-go, default port 8007).
run:
go run ./cmd/pr-af

# --- Docker (multi-stage image + Go-node compose add-on) ------------------
# The Dockerfile build context is this go/ directory (design §E.1): the SDK is
# a real require pulled by `go mod download`, so there is no SDK clone stage and
# no build-arg. The compose add-on (docker-compose.go.yml) lives at the repo
# root and joins the Python pr-af stack's external network + workspaces volume.
# Override the image tag with: make docker-build IMAGE=myrepo/pr-af-go:dev
IMAGE ?= pr-af-go:latest

# Build the multi-stage Go image. The Dockerfile expects the REPO ROOT as the
# build context (it COPYs go/go.mod, go/ ...), so the context is the parent dir.
docker-build:
docker build -f Dockerfile -t $(IMAGE) ..

# Bring up the Go node (pr-af-go:8007) as an ADD-ON to the Python stack. Start
# the Python stack first (`docker compose up` — it owns the control plane +
# shared network); this add-on joins that network as an external reference.
docker-up:
docker compose -f ../docker-compose.go.yml up --build

# Tear the Go-node add-on down.
docker-down:
docker compose -f ../docker-compose.go.yml down
Loading
Loading