From 2bb30a761b90ac4153e45fd0bf9cb8cec1e951a3 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Tue, 4 Aug 2026 16:00:59 -0400 Subject: [PATCH 1/3] feat(packages): installing this repo gives you the Go node, as pr-af MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go node is the maintained PR review node — same reasoners, same interface, one static binary, no per-node venv to build. But it lived in `go/` under the name `pr-af-go`, so getting it meant knowing to type `//go`, and anyone who had already installed the Python `pr-af` stayed on it forever. Two changes make the bare repo URL the whole story. The root manifest carries a redirect: superseded_by: https://github.com/Agent-Field/pr-af//go and the Go node drops its `-go` suffix to take the product's name. The suffix existed so the port could run beside the Python node during the migration; it is not something a user should ever have had to type. Since both manifests now declare `pr-af`, `af install https://github.com/Agent-Field/pr-af` replaces an existing install in place: same node id, same triggers, node-scoped secrets untouched because the scope name never changes. `pr-af.review` means the same thing before and after, whichever implementation is serving it. The mechanism behind the redirect is a generic manifest key in AgentField (Agent-Field/agentfield#864) — including the part that lets a successor share its predecessor's name, which is what an in-place rename needs. This repo only supplies the data. The Python package is untouched and still what `python -m pr_af.app` and docker-compose.yml run. Because the two now answer to the same ids, running both against one control plane needs an explicit NODE_ID on one of them — docker-compose.go.yml does exactly that, and is now the only place carrying `-go` ids. To install the Python node deliberately, clone the repo and install the checkout as a local path; local-path installs do not follow the redirect. Co-Authored-By: Claude Opus 5 (1M context) --- agentfield-package.yaml | 15 +++++++++++++++ go/agentfield-package.yaml | 14 +++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/agentfield-package.yaml b/agentfield-package.yaml index 8214dff..95646ee 100644 --- a/agentfield-package.yaml +++ b/agentfield-package.yaml @@ -4,6 +4,21 @@ version: 0.1.0 description: Opens draft pull requests from a task description author: Agent-Field +# The Go node in go/ is the maintained PR review node: same reasoners, same +# interface, one static binary, no per-node venv to build. Installing this repo +# installs that instead — so `af install https://github.com/Agent-Field/pr-af` +# is the one thing a user has to know, before and after the switch. +# +# go/ declares this same name deliberately, so the switch is a replacement in +# place: same node id, same triggers, node-scoped secrets kept. Only one of the +# two can be installed at a time, which is the point. +# +# This manifest stays here as the redirect, so the Python node is still what +# `python -m pr_af.app` and docker-compose run. The redirect is a git-install +# behaviour only: to install this node deliberately, clone the repo and install +# the checkout as a local path. +superseded_by: https://github.com/Agent-Field/pr-af//go + entrypoint: start: python -m pr_af.app healthcheck: /health diff --git a/go/agentfield-package.yaml b/go/agentfield-package.yaml index 2b08c2e..6eb127b 100644 --- a/go/agentfield-package.yaml +++ b/go/agentfield-package.yaml @@ -1,7 +1,13 @@ config_version: v1 -name: pr-af-go # MUST differ from root "pr-af" (installer registry is keyed by name) +# This is THE PR-AF node. It deliberately shares the root manifest's name: the +# root declares `superseded_by` pointing here, so installing this repo installs +# this package, and a user who already has the Python pr-af gets it replaced in +# place — same name, same node id, same triggers, secrets kept. Installing the +# root as a local path (the documented escape hatch) is the one way to get the +# Python node, and it necessarily takes this name over. +name: pr-af version: 0.1.0 -description: AI-Native Pull Request Review Agent (Go port) +description: AI-Native Pull Request Review Agent author: Agent-Field language: go # explicit (also auto-detected from go/go.mod) @@ -11,7 +17,9 @@ entrypoint: healthcheck: /health agent_node: - node_id: pr-af-go + node_id: pr-af + # 8007 rather than the Python node's 8004: during the changeover both may be + # running, and triggers resolve by node id, not port. default_port: 8007 user_environment: From 463f4de4912f72d84771b43f698e1fecfbc66492 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Tue, 4 Aug 2026 16:01:09 -0400 Subject: [PATCH 2/3] refactor(go): the node's own identity is pr-af, not pr-af-go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the manifest rename through the code that carries the node's runtime identity: the NODE_ID default, the port doc comments, the package docs, and every test that asserted the old default. Callers now reach `pr-af.review`, matching what the manifest registers. The 16 semantic role tags are unchanged — they were never node-identity tags, and the comment that explained the distinction by contrast with SWE-AF's `-go` role tags no longer has anything to contrast with. `-go` survives in exactly one place: docker-compose.go.yml, which runs the Go node beside the Python one against a single control plane and therefore has to override NODE_ID to keep the two distinguishable. The functional compose stack does not — it runs the Go node alone, so it uses the real id. Co-Authored-By: Claude Opus 5 (1M context) --- go/Dockerfile | 4 ++-- go/Makefile | 6 +++--- go/cmd/pr-af/main.go | 9 ++++----- go/doc.go | 6 +++--- go/internal/hitl/hax_client_test.go | 12 ++++++------ go/internal/node/calllocal_wiring_test.go | 4 ++-- go/internal/node/node.go | 5 ++--- go/internal/node/node_test.go | 12 ++++++------ go/internal/node/register.go | 4 ++-- go/internal/node/register_test.go | 10 +++++----- go/internal/node/webhook_test.go | 14 +++++++------- go/test/e2e/run.sh | 10 +++++----- go/test/functional/compose.functional.yml | 10 +++++----- go/test/functional/main_test.go | 10 +++++----- go/test/functional/reasoner_api_test.go | 4 ++-- 15 files changed, 59 insertions(+), 61 deletions(-) diff --git a/go/Dockerfile b/go/Dockerfile index 5e8ea81..deac3d7 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -4,7 +4,7 @@ # 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 . +# docker build -f go/Dockerfile -t pr-af: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 @@ -45,7 +45,7 @@ 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 \ + NODE_ID=pr-af \ HOME=/home/praf \ PATH=/home/praf/.opencode/bin:${PATH} \ XDG_DATA_HOME=/home/praf/.local/share \ diff --git a/go/Makefile b/go/Makefile index 5cd6648..0412509 100644 --- a/go/Makefile +++ b/go/Makefile @@ -33,7 +33,7 @@ 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 the PR-AF node (pr-af, default port 8007). run: go run ./cmd/pr-af @@ -42,8 +42,8 @@ run: # 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 +# Override the image tag with: make docker-build IMAGE=myrepo/pr-af:dev +IMAGE ?= pr-af: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. diff --git a/go/cmd/pr-af/main.go b/go/cmd/pr-af/main.go index 4863216..f350cf2 100644 --- a/go/cmd/pr-af/main.go +++ b/go/cmd/pr-af/main.go @@ -3,16 +3,15 @@ // surface (design §B.1), and serves the SDK handler plus the custom // /webhook/github route until SIGINT/SIGTERM. // -// Defaults: NODE_ID "pr-af-go", PORT 8007 — a distinct identity from the Python -// pr-af node (:8004) so the Go port runs as an opt-in sibling alongside Python -// against one control plane. NODE_ID / PORT env vars override. +// Defaults: NODE_ID "pr-af", PORT 8007. NODE_ID / PORT env vars override; +// docker-compose.go.yml does so when both implementations coexist. // // Boot env (T4.3 e2e / production): // // AGENTFIELD_SERVER control-plane base URL (default http://localhost:8080) // AGENTFIELD_API_KEY control-plane bearer token // AGENT_CALLBACK_URL base URL the CP uses to reach this node (else localhost) -// NODE_ID node id (default pr-af-go) +// NODE_ID node id (default pr-af) // PORT listen port (default 8007) // PR_AF_PROVIDER harness provider (default opencode) // PR_AF_MODEL harness model (env wins over the code default) @@ -33,7 +32,7 @@ import ( func main() { n, err := node.BuildAgent( - "pr-af-go", + "pr-af", "8007", "AI-Native Pull Request Review Agent", ) diff --git a/go/doc.go b/go/doc.go index b688d96..b3c782a 100644 --- a/go/doc.go +++ b/go/doc.go @@ -3,12 +3,12 @@ // The port turns a pull request (or raw diff / local repo) into a structured, // multi-dimensional code review — the same reasoner names, control-plane // surface, and HTTP API shapes as the Python pr_af package. It registers as an -// OPT-IN sibling node ("pr-af-go", default port 8007) so it can run against one -// control plane alongside the untouched Python "pr-af" node. +// maintained node ("pr-af", default port 8007). NODE_ID can be overridden when +// it runs alongside the untouched Python node. // // Layout (design §A): // -// cmd/pr-af node entry point (node id "pr-af-go") +// cmd/pr-af node entry point (node id "pr-af") // internal/afx small AgentField SDK ergonomics (typed input binding) // internal/fatal non-retryable harness-error classification // internal/harnessx the single generic Run[T] harness choke-point + schema cache diff --git a/go/internal/hitl/hax_client_test.go b/go/internal/hitl/hax_client_test.go index 31dd505..5156654 100644 --- a/go/internal/hitl/hax_client_test.go +++ b/go/internal/hitl/hax_client_test.go @@ -77,7 +77,7 @@ func ptr(s string) *string { return &s } func TestCreateRequestWireBody(t *testing.T) { clearEnv(t, haxEnvKeys...) // Deterministic sender: display name explicit, key falls back to it. - setEnv(t, "HAX_SENDER_NAME", "pr-af-go") + setEnv(t, "HAX_SENDER_NAME", "pr-af") var ( gotMethod string @@ -152,7 +152,7 @@ func TestCreateRequestWireBody(t *testing.T) { if err := json.Unmarshal(gotBody["sender"], &sender); err != nil { t.Fatalf("decode sender: %v", err) } - wantSender := map[string]any{"key": "pr-af-go", "displayName": "pr-af-go"} + wantSender := map[string]any{"key": "pr-af", "displayName": "pr-af"} if !reflect.DeepEqual(sender, wantSender) { t.Errorf("sender = %v, want %v", sender, wantSender) } @@ -162,7 +162,7 @@ func TestCreateRequestWireBody(t *testing.T) { // unset — only type, payload and the always-present sender remain. func TestCreateRequestMinimalBody(t *testing.T) { clearEnv(t, haxEnvKeys...) - setEnv(t, "NODE_ID", "pr-af-go") // sender falls back to NODE_ID + setEnv(t, "NODE_ID", "pr-af") // sender falls back to NODE_ID var gotBody map[string]json.RawMessage srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -300,10 +300,10 @@ func TestResolveSender(t *testing.T) { wantKey, wantName string }{ {name: "all unset -> pr-af/pr-af", wantKey: "pr-af", wantName: "pr-af"}, - {name: "NODE_ID drives both (design case)", nodeID: ptr("pr-af-go"), wantKey: "pr-af-go", wantName: "pr-af-go"}, - {name: "HAX_SENDER_NAME wins over NODE_ID", senderName: ptr("custom"), nodeID: ptr("pr-af-go"), wantKey: "custom", wantName: "custom"}, + {name: "NODE_ID drives both (design case)", nodeID: ptr("pr-af"), wantKey: "pr-af", wantName: "pr-af"}, + {name: "HAX_SENDER_NAME wins over NODE_ID", senderName: ptr("custom"), nodeID: ptr("pr-af"), wantKey: "custom", wantName: "custom"}, {name: "explicit key overrides name-derived key", senderName: ptr("disp"), senderKey: ptr("k-explicit"), wantKey: "k-explicit", wantName: "disp"}, - {name: "empty HAX_SENDER_NAME falls through to NODE_ID", senderName: ptr(""), nodeID: ptr("pr-af-go"), wantKey: "pr-af-go", wantName: "pr-af-go"}, + {name: "empty HAX_SENDER_NAME falls through to NODE_ID", senderName: ptr(""), nodeID: ptr("pr-af"), wantKey: "pr-af", wantName: "pr-af"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { diff --git a/go/internal/node/calllocal_wiring_test.go b/go/internal/node/calllocal_wiring_test.go index aea2907..b09b73f 100644 --- a/go/internal/node/calllocal_wiring_test.go +++ b/go/internal/node/calllocal_wiring_test.go @@ -27,7 +27,7 @@ func TestReviewHandlerForwardsLocalCaller(t *testing.T) { var got orch.Deps n := &Node{ - NodeID: "pr-af-go", + NodeID: "pr-af", reviewApp: &fakeApp{}, localCaller: sentinel, runReview: func(_ context.Context, deps orch.Deps, _ schemas.ReviewInput, _ config.ReviewConfig) (schemas.ReviewResult, error) { @@ -51,7 +51,7 @@ func TestBuildAgentWiresLocalCallerToApp(t *testing.T) { t.Setenv("PORT", "") t.Setenv("OPENROUTER_API_KEY", "") - n, err := BuildAgent("pr-af-go", "8007", "desc") + n, err := BuildAgent("pr-af", "8007", "desc") if err != nil { t.Fatalf("BuildAgent: %v", err) } diff --git a/go/internal/node/node.go b/go/internal/node/node.go index b415383..31357aa 100644 --- a/go/internal/node/node.go +++ b/go/internal/node/node.go @@ -40,7 +40,7 @@ type Node struct { // field at it and Serve mounts App.Handler() as the fallback route. App *agent.Agent - // NodeID is the resolved node id (NODE_ID env, or the pr-af-go default). + // NodeID is the resolved node id (NODE_ID env, or the pr-af default). NodeID string // AgentFieldServer is the control-plane base URL (AGENTFIELD_SERVER). The @@ -125,8 +125,7 @@ func resolvedHarnessBin(c config.AIIntegrationConfig) string { // BuildAgent constructs the PR-AF agent from the environment exactly as the // Python entry point does (app.py:26-50): // -// - NODE_ID default "pr-af-go" (opt-in sibling; distinct from the -// Python "pr-af" node so both run against one control plane). +// - NODE_ID default "pr-af" (the maintained package identity). // - AGENTFIELD_SERVER default "http://localhost:8080". // - AGENTFIELD_API_KEY -> Config.Token (control-plane bearer). // - PORT default "8007" -> ListenAddress ":8007". diff --git a/go/internal/node/node_test.go b/go/internal/node/node_test.go index c280a3b..0b3fe4c 100644 --- a/go/internal/node/node_test.go +++ b/go/internal/node/node_test.go @@ -49,7 +49,7 @@ func TestHarnessConfigPreservesExistingFields(t *testing.T) { } // TestBuildAgentFromEnv is the main.go smoke: BuildAgent resolves node identity -// from the environment (with the pr-af-go / 8007 defaults), constructs the agent +// from the environment (with the pr-af / 8007 defaults), constructs the agent // without a control plane or LLM key, and RegisterAll wires the full surface. func TestBuildAgentFromEnv(t *testing.T) { cases := []struct { @@ -62,19 +62,19 @@ func TestBuildAgentFromEnv(t *testing.T) { { name: "defaults when env unset", env: map[string]string{"NODE_ID": "", "PORT": "", "AGENTFIELD_SERVER": "", "OPENROUTER_API_KEY": ""}, - wantNodeID: "pr-af-go", + wantNodeID: "pr-af", wantServer: "http://localhost:8080", wantListen: ":8007", }, { name: "env overrides", env: map[string]string{ - "NODE_ID": "pr-af-go-canary", + "NODE_ID": "pr-af-canary", "PORT": "9107", "AGENTFIELD_SERVER": "http://cp.internal:8080", "OPENROUTER_API_KEY": "", // keep AIConfig off so New needs no key }, - wantNodeID: "pr-af-go-canary", + wantNodeID: "pr-af-canary", wantServer: "http://cp.internal:8080", wantListen: ":9107", }, @@ -85,7 +85,7 @@ func TestBuildAgentFromEnv(t *testing.T) { for k, v := range tc.env { t.Setenv(k, v) } - n, err := BuildAgent("pr-af-go", "8007", "AI-Native Pull Request Review Agent") + n, err := BuildAgent("pr-af", "8007", "AI-Native Pull Request Review Agent") if err != nil { t.Fatalf("BuildAgent: %v", err) } @@ -118,7 +118,7 @@ func TestBuildAgentWithLLMKey(t *testing.T) { t.Setenv("AGENTFIELD_SERVER", "") t.Setenv("OPENROUTER_API_KEY", "sk-or-test") - n, err := BuildAgent("pr-af-go", "8007", "desc") + n, err := BuildAgent("pr-af", "8007", "desc") if err != nil { t.Fatalf("BuildAgent with LLM key: %v", err) } diff --git a/go/internal/node/register.go b/go/internal/node/register.go index 6e6ff18..281d26d 100644 --- a/go/internal/node/register.go +++ b/go/internal/node/register.go @@ -6,8 +6,8 @@ package node // in-process but that Python still CP-registers, each tagged ["review","pr"]. // // Those 16 tags are SEMANTIC domain tags, not node-identity tags — node identity -// is carried by node_id=pr-af-go, so callers reach pr-af-go.review. They are NOT -// renamed to the node id (unlike SWE-AF's -go role tags). +// is carried by node_id=pr-af, so callers reach pr-af.review. They are not +// renamed to the node id. import ( "context" diff --git a/go/internal/node/register_test.go b/go/internal/node/register_test.go index d629462..8d796d2 100644 --- a/go/internal/node/register_test.go +++ b/go/internal/node/register_test.go @@ -92,7 +92,7 @@ func TestReviewHandlerErrorMapping(t *testing.T) { t.Run("bad-input maps to 400 with raw message, no note", func(t *testing.T) { fa := &fakeApp{} n := &Node{ - NodeID: "pr-af-go", + NodeID: "pr-af", reviewApp: fa, runReview: func(context.Context, orch.Deps, schemas.ReviewInput, config.ReviewConfig) (schemas.ReviewResult, error) { return schemas.ReviewResult{}, wrapBadInput("One of pr_url, diff_text, or repo_path is required") @@ -115,7 +115,7 @@ func TestReviewHandlerErrorMapping(t *testing.T) { t.Run("other error maps to 500 with prefix + pipeline note", func(t *testing.T) { fa := &fakeApp{} n := &Node{ - NodeID: "pr-af-go", + NodeID: "pr-af", reviewApp: fa, runReview: func(context.Context, orch.Deps, schemas.ReviewInput, config.ReviewConfig) (schemas.ReviewResult, error) { return schemas.ReviewResult{}, errors.New("kaboom") @@ -146,7 +146,7 @@ func TestReviewHandlerErrorMapping(t *testing.T) { fa := &fakeApp{} want := schemas.ReviewResult{ReviewID: "rev_abc123", PrURL: "https://example/pr/1"} n := &Node{ - NodeID: "pr-af-go", + NodeID: "pr-af", reviewApp: fa, runReview: func(context.Context, orch.Deps, schemas.ReviewInput, config.ReviewConfig) (schemas.ReviewResult, error) { return want, nil @@ -179,7 +179,7 @@ func TestReviewHandlerClampsDepthAndResolvesRepo(t *testing.T) { var seenInput schemas.ReviewInput fa := &fakeApp{} n := &Node{ - NodeID: "pr-af-go", + NodeID: "pr-af", reviewApp: fa, runReview: func(_ context.Context, _ orch.Deps, in schemas.ReviewInput, _ config.ReviewConfig) (schemas.ReviewResult, error) { seenInput = in @@ -213,7 +213,7 @@ func newTestNode(t *testing.T) *Node { t.Setenv("AGENTFIELD_SERVER", "") t.Setenv("AGENT_CALLBACK_URL", "") t.Setenv("OPENROUTER_API_KEY", "") - n, err := BuildAgent("pr-af-go", "8007", "AI-Native Pull Request Review Agent") + n, err := BuildAgent("pr-af", "8007", "AI-Native Pull Request Review Agent") if err != nil { t.Fatalf("BuildAgent: %v", err) } diff --git a/go/internal/node/webhook_test.go b/go/internal/node/webhook_test.go index c661803..b619a7d 100644 --- a/go/internal/node/webhook_test.go +++ b/go/internal/node/webhook_test.go @@ -80,7 +80,7 @@ func doWebhook(t *testing.T, n *Node, event, signature string, body []byte) (*ht func TestWebhookPing(t *testing.T) { t.Setenv("GITHUB_WEBHOOK_SECRET", "") - n := &Node{NodeID: "pr-af-go"} + n := &Node{NodeID: "pr-af"} rec, body := doWebhook(t, n, "ping", "", []byte(`{}`)) if rec.Code != 200 { t.Fatalf("status = %d, want 200", rec.Code) @@ -93,7 +93,7 @@ func TestWebhookPing(t *testing.T) { func TestWebhookSignature(t *testing.T) { const secret = "s3cr3t" t.Setenv("GITHUB_WEBHOOK_SECRET", secret) - n := &Node{NodeID: "pr-af-go"} + n := &Node{NodeID: "pr-af"} body := prComment("created", "@pr-af review", "https://github.com/octo/repo/pull/42") t.Run("valid signature passes verification (ping through)", func(t *testing.T) { @@ -121,7 +121,7 @@ func TestWebhookSignature(t *testing.T) { func TestWebhookIgnoreGates(t *testing.T) { t.Setenv("GITHUB_WEBHOOK_SECRET", "") - n := &Node{NodeID: "pr-af-go"} + n := &Node{NodeID: "pr-af"} prURL := "https://github.com/octo/repo/pull/42" cases := []struct { @@ -154,7 +154,7 @@ func TestWebhookIgnoreGates(t *testing.T) { func TestWebhookFiresAsyncReview(t *testing.T) { t.Setenv("GITHUB_WEBHOOK_SECRET", "") cp := newFakeCP(t) - n := &Node{NodeID: "pr-af-go", AgentFieldServer: cp.server.URL} + n := &Node{NodeID: "pr-af", AgentFieldServer: cp.server.URL} prURL := "https://github.com/octo/repo/pull/42" rec, resp := doWebhook(t, n, "issue_comment", "", @@ -177,8 +177,8 @@ func TestWebhookFiresAsyncReview(t *testing.T) { if cp.hitCount != 1 { t.Fatalf("CP hit %d times, want 1", cp.hitCount) } - if cp.gotPath != "/api/v1/execute/async/pr-af-go.review" { - t.Errorf("fire path = %q, want /api/v1/execute/async/pr-af-go.review", cp.gotPath) + if cp.gotPath != "/api/v1/execute/async/pr-af.review" { + t.Errorf("fire path = %q, want /api/v1/execute/async/pr-af.review", cp.gotPath) } input, ok := cp.gotBody["input"].(map[string]any) if !ok { @@ -203,7 +203,7 @@ func TestWebhookBotMentionOverride(t *testing.T) { t.Setenv("GITHUB_WEBHOOK_SECRET", "") t.Setenv("PR_AF_BOT_MENTION", "@reviewbot") cp := newFakeCP(t) - n := &Node{NodeID: "pr-af-go", AgentFieldServer: cp.server.URL} + n := &Node{NodeID: "pr-af", AgentFieldServer: cp.server.URL} prURL := "https://github.com/octo/repo/pull/7" // The default "@pr-af" no longer triggers; the configured "@reviewbot" does. diff --git a/go/test/e2e/run.sh b/go/test/e2e/run.sh index ce53d8a..614c2c0 100755 --- a/go/test/e2e/run.sh +++ b/go/test/e2e/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # run.sh — one-command E2E harness for the PR-AF Go port. # -# Runs pr-af-go.review over a LOCAL fixture git repo (a seeded 2-commit diff) with +# Runs pr-af.review over a LOCAL fixture git repo (a seeded 2-commit diff) with # the opencode HARNESS fully mocked, so the deterministic bulk of the review # pipeline (anatomy -> meta selectors -> review dimensions -> evidence -> # adversary -> compound/obligations -> synthesis -> output) runs with ZERO LLM @@ -55,7 +55,7 @@ CP_PORT=18080 NODE_PORT=18017 CP_URL="http://localhost:$CP_PORT" NODE_URL="http://localhost:$NODE_PORT" -NODE_ID="pr-af-go" +NODE_ID="pr-af" KEEP=0 [[ "${1:-}" == "--keep" ]] && KEEP=1 @@ -168,9 +168,9 @@ fi ok "control plane healthy ($CP_URL)" # --------------------------------------------------------------------------- -# 4. Start the pr-af-go node with the opencode shim wired in +# 4. Start the pr-af node with the opencode shim wired in # (PR_AF_OPENCODE_BIN points the harness straight at the shim; PATH is belt -# and suspenders). NODE_ID=pr-af-go opts into the Go sibling identity. +# and suspenders). NODE_ID=pr-af makes the expected identity explicit. # --------------------------------------------------------------------------- log "starting pr-af node on :$NODE_PORT (shim=$SHIM/opencode, cwd=$RUN_DIR)" # cwd matters: harness calls made with an empty Cwd (intake fallback, dedup / @@ -207,7 +207,7 @@ for _ in $(seq 1 30); do sleep 1 done [[ "${RC:-0}" -ge 1 ]] || { err "node never appeared in CP capabilities (see $RUN_DIR/node.log)"; exit 1; } -ok "pr-af-go node registered (pid $NODE_PID)" +ok "pr-af node registered (pid $NODE_PID)" # --------------------------------------------------------------------------- # 5. Kick off the async review (repo_path + dry_run=true) and poll to terminal diff --git a/go/test/functional/compose.functional.yml b/go/test/functional/compose.functional.yml index 91e9bad..2b82a13 100644 --- a/go/test/functional/compose.functional.yml +++ b/go/test/functional/compose.functional.yml @@ -18,8 +18,8 @@ # keeps a single source of truth and avoids the version-sensitive `!override` # merge tag. # -# The node registers under the Go port's opt-in-sibling identity: -# pr-af-go -> node id "pr-af-go", :8007 (published on host :28017) +# The node registers under the maintained package identity: +# pr-af -> node id "pr-af", :8007 (published on host :28017) # # Zero secrets required: the functional tests are registration parity + a # fail-fast review (a nonexistent repo_path errors at `git diff` before any @@ -36,7 +36,7 @@ services: volumes: - agentfield-data:/data - pr-af-go: + pr-af: build: # Paths are relative to THIS file's dir (go/test/functional); ../../.. points # the build context at the repo root so the go/ module is in context (the @@ -46,9 +46,9 @@ services: environment: - AGENTFIELD_SERVER=http://control-plane:8080 - AGENTFIELD_API_KEY=${AGENTFIELD_API_KEY:-} - - NODE_ID=pr-af-go + - NODE_ID=pr-af - PORT=8007 - - AGENT_CALLBACK_URL=http://pr-af-go:8007 + - AGENT_CALLBACK_URL=http://pr-af: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:-} diff --git a/go/test/functional/main_test.go b/go/test/functional/main_test.go index 48d38d2..89b6c7c 100644 --- a/go/test/functional/main_test.go +++ b/go/test/functional/main_test.go @@ -2,7 +2,7 @@ // Package functional holds the black-box / functional parity tests for the PR-AF // Go port (design §F, work-breakdown T4.3). They exercise the *live* stack — a -// control plane plus the Go node (pr-af-go :8007) — brought up via the +// control plane plus the Go node (pr-af :8007) — brought up via the // self-contained compose.functional.yml, and assert the parity contracts the // Python→Go port must preserve: // @@ -12,7 +12,7 @@ // ["review","pr"] where the control plane exposes per-reasoner tags // (TestRegistrationParity, hardcoded from the PYTHON surface, NOT the Go // register.go); -// - a deterministic (no-LLM) pr-af-go.review against a nonexistent repo_path +// - a deterministic (no-LLM) pr-af.review against a nonexistent repo_path // fails fast (before any harness / .ai() call) and the terminal execution // record carries a non-succeeded status plus an error message — the review // error-shape contract (TestReviewErrorShape, design §F V2). @@ -53,8 +53,8 @@ const ( composeFile = "go/test/functional/compose.functional.yml" // composeProject isolates this stack's containers/volumes/network from any - // concurrently running pr-af / pr-af-go compose project. - composeProject = "pr-af-go-functional" + // concurrently running production compose projects. + composeProject = "pr-af-functional" // Host ports are deliberately uncommon (see compose.functional.yml) to avoid a // dev `af server` / dev stack squatting on the CP's host port, which silently @@ -62,7 +62,7 @@ const ( cpBaseURL = "http://localhost:28080" prafBaseURL = "http://localhost:28017" - prafNodeID = "pr-af-go" + prafNodeID = "pr-af" // Generous ceilings: the Go image is a multi-stage build that runs // `go mod download` against the module proxy, so a cold `up --build` can take diff --git a/go/test/functional/reasoner_api_test.go b/go/test/functional/reasoner_api_test.go index d15613d..975038e 100644 --- a/go/test/functional/reasoner_api_test.go +++ b/go/test/functional/reasoner_api_test.go @@ -11,7 +11,7 @@ import ( "time" ) -// TestReviewErrorShape drives the full CP async surface for pr-af-go.review with a +// TestReviewErrorShape drives the full CP async surface for pr-af.review with a // nonexistent repo_path (design §F V2). A local repo_path that is neither a // directory nor a clonable URL makes the orchestrator's intake `git diff` fail // IMMEDIATELY — before any harness (opencode) or .ai() call — so the whole path is @@ -29,7 +29,7 @@ func TestReviewErrorShape(t *testing.T) { input := map[string]any{ // Guaranteed not a git repo (and not a clonable URL) inside the container. - "repo_path": "/nonexistent/pr-af-go-functional-probe", + "repo_path": "/nonexistent/pr-af-functional-probe", "dry_run": true, } From 0576acd0f31830c0c7c8502f16ee8da78df1926b Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Tue, 4 Aug 2026 16:01:22 -0400 Subject: [PATCH 3/3] docs: the install story is the bare repo URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both READMEs still told the reader the Python node was the default and that the Go node was an opt-in sibling reached by typing `-go` into the reasoner path. Neither is true once the root manifest redirects: `af install https://github.com/Agent-Field/pr-af` gives you the Go node as `pr-af`, and an existing Python install is replaced in place. Documents the local-path escape hatch in both files, since that is now the only way to install the Python node deliberately, and keeps the Go add-on Compose file described as what it is — the way to run both at once during a changeover, which is why it still carries `-go` ids. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 26 +++++++++++++++++--------- go/README.md | 47 ++++++++++++++++++++++------------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 6b2c35d..d88dff2 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ af install https://github.com/Agent-Field/pr-af af run pr-af ``` -`af install` clones the repo, provisions an isolated Python environment, and registers the `pr-af` node with your control plane. On first `af run` you're prompted for the required secrets — `OPENROUTER_API_KEY` and `GH_TOKEN` — which are stored encrypted and reused across every node, so you enter each only once. Then review a PR: +`af install` follows the repository manifest to the maintained Go package and registers it as the `pr-af` node with your control plane. If an older Python `pr-af` is installed, it is replaced in place, retaining the same node id, triggers, and node-scoped secrets. On first `af run` you're prompted for the required secrets — `OPENROUTER_API_KEY` and `GH_TOKEN` — which are stored encrypted and reused across every node, so you enter each only once. Then review a PR: ```bash af call pr-af.review --in '{"pr_url": "https://github.com/owner/repo/pull/123"}' @@ -207,6 +207,14 @@ af call pr-af.review --in '{"pr_url": "https://github.com/owner/repo/pull/123"}' New to AgentField? Install the control plane first with `curl -fsSL https://agentfield.ai/install.sh | bash`, or use one of the options below. +To install the Python node deliberately, clone this repository and install the +checkout as a local path. Local-path installs do not follow `superseded_by`: + +```bash +git clone https://github.com/Agent-Field/pr-af +af install ./pr-af +``` + ### Deploy with Railway (fastest) [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/pr-af) @@ -335,12 +343,12 @@ When the writer and the reviewer are the same intelligence, the pull request gat --- -## Go implementation (opt-in) +## Go implementation -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). +The maintained node lives under [`go/`](go/README.md), and installing the bare +repository URL gives you this implementation as `pr-af` on its default port +`8007`. The Python implementation remains available through `python -m +pr_af.app`, the root Docker Compose stack, or the local-path install escape +hatch above. The Go add-on Compose file explicitly uses `pr-af-go` only so both +implementations can run against one control plane during a changeover. Build, +run, and Docker/compose docs live in [`go/README.md`](go/README.md). diff --git a/go/README.md b/go/README.md index c8c42f5..74b6b17 100644 --- a/go/README.md +++ b/go/README.md @@ -1,35 +1,35 @@ # PR-AF — Go node -A Go port of the PR-AF agentic code-review node. It registers the same reasoner +A Go implementation of the PR-AF agentic code-review node. It registers the same reasoner surface under the same names as the Python node, exposes a byte-compatible HTTP API, and reports every pipeline phase as its own tracked execution, so the control-plane DAG UI renders the same multi-node orchestration graph as the Python node (see [Pipeline DAG on the control plane](#pipeline-dag-on-the-control-plane)). -The Python package under `src/pr_af/` is untouched; this port lives entirely +The Python package under `src/pr_af/` is untouched; this implementation lives entirely under `go/`. One binary: | Binary | Node ID | Default port | Role | |----------|------------|--------------|----------------------------------------| -| `pr-af` | `pr-af-go` | `8007` | Full review pipeline (intake → review) | +| `pr-af` | `pr-af` | `8007` | Full review pipeline (intake → review) | Module path: `github.com/Agent-Field/pr-af/go`. -## Opt-in alongside Python +## Install -The Python node is the **default**: `pr-af` on `:8004`, unchanged. This Go port -registers **separately** under a distinct identity — `pr-af-go` on `:8007` — so -both nodes can run against **one** control plane at the same time. Nothing is -replaced; callers **opt in** by targeting the `-go` reasoner path, e.g. +Installing the bare repository URL follows the root manifest's redirect to this +package and registers the Go node as `pr-af` on `:8007`: ```bash -curl -X POST http://localhost:8080/api/v1/execute/async/pr-af-go.review \ - -H 'Content-Type: application/json' \ - -d '{"input":{"pr_url":"https://github.com/owner/repo/pull/123"}}' +af install https://github.com/Agent-Field/pr-af +af run pr-af +af call pr-af.review --in '{"pr_url":"https://github.com/owner/repo/pull/123"}' ``` -`NODE_ID` / `PORT` still override the defaults if you want a different id/port. +To install the Python node deliberately, clone the repository and run `af +install ./pr-af`; local-path installs do not follow the redirect. `NODE_ID` / +`PORT` still override the Go defaults if you want a different id/port. ## Pipeline DAG on the control plane @@ -93,7 +93,7 @@ make vet # go vet ./... make test # go test ./... make check # vet + test make fmt # gofmt -w . -make run # run the node (pr-af-go, :8007) +make run # run the node (pr-af, :8007) ``` `make run` needs a control plane reachable at `AGENTFIELD_SERVER` (default @@ -113,7 +113,7 @@ The build context is the **repo root** so the `go/` module is in context: ```bash # from the repo root -docker build -f go/Dockerfile -t pr-af-go:latest . +docker build -f go/Dockerfile -t pr-af:latest . ``` ### Compose: opt-in add-on to the Python stack @@ -153,7 +153,7 @@ The node is configured entirely through the environment. | `GH_TOKEN` | GitHub token (`repo` scope) for reading PRs and posting reviews | | `AGENTFIELD_SERVER` | Control-plane URL (default `http://localhost:8080`) | | `AGENTFIELD_API_KEY` | Control-plane API key (if the CP has auth enabled) | -| `NODE_ID` | Node ID (default `pr-af-go`) | +| `NODE_ID` | Node ID (default `pr-af`) | | `PORT` | Listen port (default `8007`) | | `PR_AF_PROVIDER` | Harness provider (default `opencode`) | | `PR_AF_MODEL` | Harness model (default `openrouter/moonshotai/kimi-k2.5`) | @@ -167,18 +167,15 @@ 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). -## Deployment: `af install --path go` +## Deployment: `af install` -Because the SDK is a committed real `require` (no out-of-tree `replace` for the -installer to reject), the Go node **can** be installed via the AgentField -package installer, pointing it at the `go/` subdirectory: +Because the root package redirects git installs here, install the Go node with +the repository's bare URL: ```bash -af install https://github.com/Agent-Field/pr-af --path go +af install https://github.com/Agent-Field/pr-af ``` -This reads `go/agentfield-package.yaml` (node id `pr-af-go`, default port -`8007`) and builds `./cmd/pr-af`. The `--path` subdirectory selector requires -**agentfield ≥ v0.1.108** (the installer's `--path` support, merged in -agentfield#750); on an older control plane, prefer the Docker image / compose / -binary path above. +This resolves to `go/agentfield-package.yaml` (node id `pr-af`, default port +`8007`) and builds `./cmd/pr-af`. A prior Python `pr-af` installation is +replaced in place, retaining its node id, triggers, and node-scoped secrets.