feat: opt-in Go port of PR-AF on the AgentField Go SDK#53
Conversation
Module github.com/Agent-Field/pr-af/go pinned to the AgentField Go SDK at agentfield#750's head via a real pseudo-version require (no replace, so 'af install --path go' can build the package tree as copied). afx.Bind and the fatal-error classifier are verbatim from the SWE-AF Go port; harnessx.Run[T] is copy-adapted with the scout-credential injection removed (PR-AF has no credential store). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
go/Dockerfile (multi-stage, repo-root context, plain 'go mod download' — the SDK resolves from the module proxy so no clone stage), byte-copied docker-entrypoint.sh (opencode.json generation from PR_AF_MODEL), docker-compose.go.yml as an add-on joining the Python stack's external network/volume, go/agentfield-package.yaml (name pr-af-go, language go) for 'af install <repo> --path go', READMEs positioning Python as the default and pr-af-go (:8007) as opt-in, and a Go CI job. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pydantic-parity structs with snake_case tags, pointer optionals, and UnmarshalJSON default-seeding (seed-then-alias idiom); the canonical severity alias map from schemas/severity.py with coercing unmarshal. Config mirrors config.py's numeric tables with call-time env reads and the _resolve_budget_caps cascade (explicit arg > env > 2.0/300); the code-level model default stays minimax/minimax-m2.5 while deploy artifacts default to kimi — env wins, matching Python exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Direct port of hitl/client.py: sender manifest always emitted (displayName camelCase, call-time env resolution with NODE_ID fallback), 120s hard timeout on create_request, publicKey omitted so values round-trip in plaintext, and the AGENTFIELD_PUBLIC_URL > agent-server > AGENTFIELD_SERVER webhook-URL precedence. Pause goes through the SDK-native Agent.Pause via a one-method Pauser seam. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports scoring.py: score_findings with positional f_%03d ids and stable descending sort, deduplicate_exact, determine_review_event, and the adversary missed-trap synthesis. Keeps scoring's own severity alias map (medium->important, low->suggestion) deliberately divergent from the canonical schemas map, as in Python. pyRound reproduces CPython round-half-to-even via FormatFloat (0.0625 -> 0.062, where math.Round would give 0.063). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports diff_engine.py verbatim, with parity verified by running 15 fixture diffs through the real Python module and baking the exact model_dump() output into the Go assertions. Preserves Python quirks: renames surface as modified (files_renamed only set on the GitHub API path), pure-rename/deleted/binary edge cases yield empty paths, and str.splitlines() semantics via a dedicated helper. The one documented divergence is a deterministic tie-break in primaryLanguage where Python's own answer is set-iteration-order nondeterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports github/client.py: PR URL parsing, FetchPR with 4-attempt retry (2s*(attempt+1) backoff on transport/5xx/403/429, fail-fast otherwise), files/commits pagination at per_page=100, diff via the v3.diff Accept header, PostReview with path/line comment filtering, and a typed APIError so the orchestrator can detect the 422 own-PR case. App auth mints an RS256 JWT (iat=now-60, exp=now+600) and caches installation tokens with a 5-minute early refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports evidence.py (grep-backed caller/import extraction with byte-identical grep invocations, order-preserving semaphore-of-10 fan-out, dimension packs, path/hunk helpers) and blast_radius.py (deliberately Python-import-limited dependency graph — relative imports stay unresolved and 'from pkg import mod' resolves the package, as upstream). Golden values captured by running the real Python implementation against the same fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 25 LLM prompt builders (16 reasoners + merge gate + polish + coverage-gap) ported with byte-for-byte golden tests over 51 fixtures. pyjson reproduces json.dumps separators, insertion-ordered keys, ensure_ascii escapes, and Python float repr; pyRepr covers single-quoted list interpolation. scripts/gen_golden.py is committed so fixtures are reproducible from the repo — the generator documents its invocation and is deterministic across runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports merge_gate.py and polish.py: per-finding parallel .ai() calls with order-preserving pre-indexed fan-out (Python's gather without return_exceptions is safe here because each worker catches internally), verdict parsing tolerant of fences/prose/garbage with the exact 'gate parse error'/'gate non-object'/'gate error' fallbacks, rune-accurate 400-char reason truncation, and blocking=false on every failure path. Polish keeps the original comment on any failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports hitl/review_gate.py: the pr-af-review-v1 hax payload (the system's one camelCase surface — per-finding entries re-normalize severity at the boundary so a stray 'high' cannot re-trigger the zod-enum 422), decision parsing (post honors findings_to_post subset, rerun carries instructions, expired/error/rejected map to reject), and RequestReviewApproval which never raises — payload/create/pause failures return reject decisions with the exact Python instruction strings. Intent cleaning is rune-aware for non-ASCII PR bodies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports reasoners/harnesses.py: typed inputs with seeded defaults,
private harness-result structs, per-reasoner parse-failure fallbacks
(intake's literal {}, planning's minimal plan, keep-all gates), lens
forcing on the meta selectors, adversary skepticism escalation, and
the .pr-af-context file writers. Prompt assembly verified byte-equal
against the running Python reasoners with a mocked harness. Adds
prompts.NewOMap/PyJSON exports the builders were missing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports orchestrator.py: the streaming reviewer/layer pipeline (channel port of the asyncio.Queue path — the layer starts consuming before all reviewers finish), order-preserving fan-outs for positional IDs, batched adversary, coverage/consistency parallel phases, compound analysis with deterministic cluster selection, synthesis, byte-exact summary Markdown (banker's rounding + Python float rendering, golden generated from the real _format_summary), the HITL revision loop, the 422 own-PR downgrade, repo resolution/checkout with verbatim git error strings, and the inert cost / live wall-clock budget gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BuildAgent reads env into agent.Config (node_id pr-af-go, port 8007, AGENT_CALLBACK_URL as PublicURL); Serve binds the listener before Initialize so the control plane's post-register health check reaches a live server, and routes /webhook/github (HMAC verify, ping, bot-mention and PR gates, async re-fire at the CP) ahead of the SDK handler. The review handler reproduces both max_review_depth clamps and the exact Python error mapping: ErrBadInput -> 400 with the raw message, anything else -> 'Review pipeline failed' note + 500 with the 'review execution failed: ' prefix. Also unwraps computeRepoDiff's bad-input error to the raw stderr message so the 400 body matches Python on the repo-diff path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Functional (-tags functional) suite boots the compose stack on
collision-free host ports and asserts, against the live control plane,
the exact 17-name registration surface with tags (via the discovery
capabilities endpoint — the nodes/{id} route the design assumed does
not exist) and the fail-fast error shape of review with a nonexistent
repo_path. test/mockcli is a deterministic opencode drop-in that
speaks the SDK's JSON-stream + output-file protocol; test/e2e/run.sh
drives a full review of a fixture repo through it (the four .ai()
gates still need a real OPENROUTER_API_KEY — documented, skip-clean
without it). Executed here: functional 3/3 PASS with real docker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SDK runner validates mock output against the invopop schema (pointer fields are required and non-nullable), sends standalone retry prompts, and names the output file relatively when Cwd is empty — the shim now satisfies all three (never writing on retry or unknown prompts) and run.sh polls the discovery endpoint that actually exists. Executed result: full review of the fixture repo succeeds with 4 findings, all phases fired, zero GitHub writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…text Malformed numeric env values now fail loud like Python instead of silently defaulting: PR_AF_MAX_COST_USD/PR_AF_MAX_DURATION_SECONDS surface as HTTP 400 with Python's ValueError message shape (request path), and PR_AF_MAX_TURNS-class variables fail BuildAgent at boot (Python raises at module import). HITL metadata now carries the real executionId via the SDK's exported ExecutionContextFrom accessor. make docker-build uses the repo root as its context, matching what the Dockerfile COPYs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pinned SDK strictly validates harness output against the schema map (the prior doc comment predated that change), and invopop's reflection marked every field required, pointers non-nullable, and extra keys forbidden — so Python-valid model output (omitted defaulted fields, suggestion:null, extra keys) was rejected and retried. The 13 harness destination types now use embedded schemas generated from the real pydantic models by the committed gen_schemas.py (invopop remains only as a fallback for unregistered types). The one deviation from raw model_json_schema() is deliberate: the Severity enum is stripped to plain string because pydantic's BeforeValidator coerces synonyms rather than rejecting them, and Go normalizes on unmarshal the same way. Drift tests cross-check every fixture against the Go structs' json tags, and a two-way proof test shows the old schema rejecting — and the new schema accepting — the same Python-valid document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed all four review findings (commits High — harness schemas stricter than Python: fixed in Medium — Medium-low — HITL Low — malformed numeric env: fixed in All gates re-run green after the fixes: |
Summary
Complete 1:1 port of PR-AF from Python to Go using the AgentField Go SDK, mirroring the SWE-AF port (Agent-Field/SWE-AF#94). The Go node is an opt-in sibling: it registers as
pr-af-go(:8007) so both stacks run against one control plane simultaneously. The Python implementation stays the default and is byte-untouched — every diff lives undergo/, plusdocker-compose.go.yml, one root-README section, and a new Go CI job.Callers opt in by targeting the
-goreasoner path, e.g.POST /api/v1/execute/async/pr-af-go.review;NODE_ID/PORTenv overrides remain available.Install story
af install <repo>installs the Python node from the root manifest.af install <repo> --path goinstalls the Go node fromgo/agentfield-package.yaml(name: pr-af-go,language: go) — the control plane compilesbin/pr-afat install time. Requires the--pathselector + Go-package support from feat: af install --path selector + Go SDK harness fixes agentfield#750. Verified end-to-end against anafbinary built from that branch: manifest resolution from thego/subtree, install-time compile, registration aspr-af-go, clean uninstall.docker build -f go/Dockerfile ., ordocker compose -f docker-compose.go.yml up -das an add-on to the running Python stack (joins its network/volume as external references). Works independently of #750.The committed
go.modpins the SDK by real pseudo-version (sdk/go v0.0.0-20260710152001-45311737c298, the #750 head) with noreplacedirective — it resolves from the module proxy, which is what makes theaf install --path gobuild work without vendoring. Bump to the merged-main SHA once #750 lands.Parity contract honored
review(untagged, with input schema) + 16 tagged["review","pr"]. Asserted by a functional test against a live control plane.app.call: like Python, the 16 internal reasoners run in-process (goroutines + errgroup/semaphore reproducing asyncio.gather order-preservation and the streaming reviewer→layer queue); onlyreviewis externally driven.model_dump()key sets reproduced per reasoner (pydantic non-zero defaults seeded viaUnmarshalJSON); the hax HITL payload stays the one camelCase surface, with severity re-normalized at that boundary.schemasmap vsscoring's private map that mapsmedium→important,low→suggestion).go/scripts/gen_golden.py) so goldens are reproducible from the repo.minimax/minimax-m2.5while deploy artifacts default toopenrouter/moonshotai/kimi-k2.5(env wins); the$cost ceiling stays inert (only the wall-clock cap is live); Pythonround()banker's-rounding reproduced in scoring and the summary Markdown is byte-exact vs_format_summary(golden generated from the running Python).Agent.Pause(webhook-resumed) + direct hax REST client; the review gate never raises — every failure path returns a reject decision with the exact Python instruction strings.Test plan
go build ./... && go vet ./... && go test ./...— whole module green (16 packages with tests; orchestrator + evidence also-race-clean);gofmt -lempty.ruff checkclean,pytest69/69.go/Dockerfile).go test -tags functional ./test/functional/) against a real dockerized control plane: health, exact 17-name registration parity + tags, fail-fast error shape. 3/3 PASS, stack torn down cleanly.go/test/e2e/run.sh): full review of a fixture repo through a mockopencodeshim against a real control plane — succeeded with 4 findings, all phases fired (anatomy → meta ×3 → review dimensions → evidence → adversary → compound/obligations → synthesis → output). The four.ai()gate calls (intake/coverage/merge-gate/polish) hit OpenRouter for real — cheap, but the harness needsOPENROUTER_API_KEY(documented in the script header; skips cleanly without it).af install <repo> --path goverified with a #750-builtaf(see Install story).Known accepted differences (documented in code)
primary_languageand compound-cluster grouping); JSON whole-value floats render0instead of0.0(numerically identical; the Markdown path uses Python-parity formatting).Cwdshare one<cwd>/.agentfield_output.json(both SDKs use the same deterministic path), so concurrent same-repo harness calls can race on write/cleanup. The port reproduces Python's behavior bug-for-bug per the parity contract; a proper fix (per-call isolation viaProjectDir/unique cwd) is a follow-up that should land in Python and Go together.Docs
go/README.mdcovers build/run/compose/install; the root README gains one "Go implementation (opt-in)" section positioning Python as the default.🤖 Generated with Claude Code