Skip to content

feat: full Go port of SWE-AF on the AgentField Go SDK#94

Draft
AbirAbbas wants to merge 55 commits into
mainfrom
feat/go-port
Draft

feat: full Go port of SWE-AF on the AgentField Go SDK#94
AbirAbbas wants to merge 55 commits into
mainfrom
feat/go-port

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Complete 1:1 port of SWE-AF from Python to Go using the AgentField Go SDK, per the full-transition plan. The Python implementation stays untouched during the transition; the Go module lives under go/ with two binaries (swe-planner :8003, swe-fast :8004).

Parity contract honored:

  • Every reasoner is separately registered under its exact Python name — swe-planner exposes 30 (build, plan, execute, resolve, resume_build + 25 roles), swe-fast exposes 29 (25 roles + fast build/fast_plan_tasks/fast_execute_tasks/fast_verify). No grouping, no simplification.
  • All inter-reasoner calls go through agent.Call (control-plane routed), so the execution DAG renders identically — the control plane builds edges solely from X-Run-ID/X-Parent-Execution-ID, which the Go SDK sets identically to Python.
  • API surface unchanged: same input parameter names/defaults, same snake_case result JSON (pydantic non-zero defaults reproduced via UnmarshalJSON seeding), same registration tags as Python's routers.
  • Prompts ported byte-verbatim (golden tests fixture-generated from the live Python builders), checkpoint JSON round-trips Python-written checkpoint.json for resume compatibility, verbatim error strings and note messages throughout.

Update (2026-07-10): repositioned as an opt-in sibling, not a drop-in replacement

The Go port no longer shadows the Python node's identity. It now registers under distinct default identities so both stacks can run against a single control plane at the same time: swe-planner-go on :8005 and swe-fast-go on :8006 (the Python nodes remain the default — swe-planner :8003 / swe-fast :8004 — and are completely untouched). Callers opt in per request by targeting the -go reasoner path, e.g. POST /api/v1/execute/async/swe-planner-go.build; NODE_ID/PORT env overrides still work.

docker-compose.go.yml is now an add-on to the Python stack rather than a parallel copy of it: bring up the Python stack first (docker compose up -d, which owns the control plane), then docker compose -f docker-compose.go.yml up -d to add the Go nodes (they join the Python stack's network/volume as external references). All Python code, the root Dockerfile/compose, and existing instructions are byte-for-byte unchanged — every diff lives under go/, docker-compose.go.yml, and a single new root-README section.

Also since the last push: a zero-LLM e2e harness (go/test/e2e-fast/run.sh, mock claude CLI) that runs the full pipeline (plan → DAG → control loops → verify → push → PR → CI gate) in ~60s and passes against the new -go identity, plus a CI-gate fix (gh pr checks invalid headSha JSON field; benign "no checks reported" now yields no_checks instead of error).

Key implementation notes

  • HITL: the Go SDK has no Agent.Pause(); the plan-approval and ask-user flows use the SDK's poll-based RequestApproval/WaitForApproval (execution still shows waiting in the UI). Hax is called via its REST API directly, replicating the Python SDK's payload.
  • ReasonerFailed(result=...): replicated without SDK changes — the node posts status=failed + structured result + error to the executions status endpoint, then errors (contract verified against a live control plane).
  • Deployment: af install is Python-only, so the Go nodes ship via go/Dockerfile (multi-stage; agentfield SDK pinned via AGENTFIELD_SDK_REF build arg for cache-busting) and docker-compose.go.yml mirroring the Python stack. Dev builds use a go.work + replace directive against the sibling repo.
  • Bug found by the functional suite: the nodes now register AGENT_CALLBACK_URL as their public URL — without it every containerized deployment got 504 agent_unreachable from the control plane.

Known accepted differences (documented in go/docs/port/)

  • Poll-based HITL resume (latency) instead of webhook-instant.
  • Harness metrics carry no cost_usd (Go SDK gap; candidate SDK patch).
  • Harness schema_mode="incremental" has no Go counterpart (single-shot + schema retries instead).
  • A few empty-collection edges marshal null where pydantic emits [] (role fallbacks seed the important ones explicitly).

Test plan

  • go build ./... && go vet ./... && go test ./... — whole module green (27 packages, includes -race on concurrency-heavy packages).
  • Black-box functional suite (go test -tags functional ./go/test/functional/) against a live compose stack: health, exact registration parity (30+29 names), result key-set parity, failed-with-result status contract. All PASS; stack torn down after.
  • docker build -f go/Dockerfile . succeeds; docker compose -f docker-compose.go.yml config validates.
  • Python suite untouched and green in a clean env (python3.12 -m pytest tests/, compileall): zero Python files changed; the only local failures reproduce identically on unmodified main (env leakage from .env via load_dotenv + a sibling repo's editable install shadowing tests.conftest) and cannot occur in CI.
  • LLM-gated end-to-end build + DAG-parity test (SWE_FUNCTIONAL_LLM=1, implemented in go/test/functional/build_llm_test.go) — requires an Anthropic key; run before flipping production traffic.

Docs

go/docs/port/ carries the design doc, work breakdown, and the two research reports (Python inventory, Go SDK gap analysis) the port was executed from; go/README.md covers build/run/deploy.

Update: external-review fixes (2026-07-10)

Adversarial review of the port surfaced six fixes, applied against the pinned SDK
(agentfield PR #750, feat/install-path-selector @ 45311737), which now adds a
native result-carrying agent.ReasonerFailed, real JSON-schema validation inside
the harness retry loop, and codex/opencode structured-output fixes. This supersedes
the two "known differences" above about the manual ReasonerFailed carrier and
null empty collections.

  1. Null lists → [] — a reflection walker (schemas.EmptyForNilSlices) plus
    DAGState.MarshalJSON and harnessx.Run normalization ensure every list field
    serialises as [] (pydantic rejects present-but-null lists); dict|None fields
    (workspace_manifest, git_init_result, tests_passed, …) legitimately stay null.
  2. Pydantic-faithful schemas — now that the SDK validates harness output against
    the reflected schema, schemaFor adds enum constraints (via JSONSchemaExtend),
    replaces invopop's all-fields required with the pydantic no-default set, relaxes
    additionalProperties, and makes Optional/map fields nullable — so valid output
    is no longer over-rejected while invalid enum values now are.
  3. QA-synthesizer LLM pathBuildAgent now sets AIConfig (guarded; nil when
    no key), making the direct-LLM branch reachable; short model aliases
    (haiku/sonnet/opus) are mapped to provider ids when the client targets
    OpenRouter. Env vars documented in .env.example and docker-compose.go.yml.
  4. Native agent.ReasonerFailed — the empty-build guard returns the SDK's
    result-carrying &agent.ReasonerFailed; the manual internal/reasonerfail
    POST-then-error carrier is deleted.
  5. Data race + gofmt — mutex-guarded the fast/orch concurrent test doubles so
    go test -race ./... is clean; formatted the two gofmt-flagged files.
  6. SDK pin + CI — bumped AGENTFIELD_SDK_REF to 45311737 (Dockerfile + Makefile,
    plus the new indirect santhosh-tekuri/jsonschema dep the SDK harness pulls in),
    and added a go CI job (sparse-clone the SDK at the pin, then
    gofmt/build/vet/test -race; the existing Python job is untouched).

Also normalized mockclaude output so the zero-LLM e2e harness stays schema-valid
under the new SDK validation. Validation: gofmt / go build / go vet /
go test -race green in both workspace and GOWORK=off modes;
docker compose -f docker-compose.go.yml config valid; the full mock e2e pipeline
(go/test/e2e-fast/run.sh) passes end-to-end against a live control plane with
zero schema-validation retries; Python suite unchanged and green.


🤖 Generated with Claude Code

AbirAbbas and others added 30 commits July 9, 2026 13:09
Module github.com/Agent-Field/SWE-AF/go with replace directive to the
sibling agentfield Go SDK. Stub entrypoints for swe-planner (:8003) and
swe-fast (:8004) wire env -> agent.Config. afx.Bind[T] marshals a
reasoner input map into a typed struct, routing through UnmarshalJSON
so pydantic-style defaults seed correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ucts

Every planning/execution/fast/hitl data model ported with exact
snake_case JSON tags and no omitempty. 13 structs with non-zero
pydantic defaults get defaultXxx() + UnmarshalJSON seeding so
zero-input unmarshal matches Python (e.g. CoderResult.complete=true,
IssueGuidance.needs_new_tests=true). Enums are typed string consts
with exact Python values; KNOWN_SERVICES ported verbatim;
BuildResult.MarshalJSON computes pr_url like the Python model_dump
override. Round-trip tests cover a Python-shaped checkpoint fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… mapping

Verbatim port of runtime/providers.py including the provider/adapter
asymmetry (claude_code -> "claude" vs "claude-code") and exact
'Unsupported runtime provider' error text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fatal ports the 13 non-retryable harness error patterns and
FatalHarnessError; envelope.UnwrapCallResult reproduces
unwrap_call_result semantics including verbatim failure messages and
the error_message -> error -> "unknown" fallback chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run-scoped in-memory credential store with env injection (scoped
values override base), matching hitl/credentials_store.py: values are
never persisted or returned; empty values filtered; mutex-guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replicates ReasonerFailed(result=...) without SDK changes: best-effort
POST of status=failed + structured result + error to the executions
status endpoint, then a plain error return so the SDK marks the
execution failed while the control plane keeps the result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Byte-identical port of the Exa websearch guardrail, applied only when
OPENCODE_ENABLE_EXA=1 and EXA_API_KEY are set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…application

Kahn's level scheduling with deterministic insertion ordering matching
Python dict semantics, transitive downstream lookup, same-level file
conflict detection, sequence numbering, and ApplyReplan with
current_level reset. Cycle errors reproduce Python's list repr.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run[T] is the single choke-point for role LLM calls: invopop schema
reflection (cached), scoped-credential env injection, fatal-error
classification before parse handling, and default-seeded fallback when
structured output fails to parse. RoleOptions maps role config onto
harness.Options with the adapter provider string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verbatim port of ci_gate.py: pending/failure bucket classification,
SHA anchoring that refuses a verdict until checks for the head SHA
appear, per-check failed-log excerpts via gh run view, and byte-equal
summary strings. Poll loop honors ctx cancellation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pper

Direct hax API client replicating the Python SDK's create-request body
(camelCase to_payload, publicKey omitted for plaintext webhook values),
RequestUserInputAndPause built on the SDK's RequestApproval +
WaitForApproval polling (execution shows waiting in the UI), verbatim
decision-to-status mapping and values extraction, budget-bounded
RunWithAskUser re-invocation, and deterministic service detection for
the environment scout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odel resolution

Verbatim legacy-key rejection and repo normalization errors, strict
decode matching pydantic extra=forbid, and the full model resolution
cascade (runtime base < SWE_DEFAULT_MODEL/AI_MODEL/HARNESS_MODEL env <
models.default < models.<role>) across the 17 role model fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, issue_writer

Verbatim port with pyStr/pyRepr helpers so f-string interpolation
(list reprs, True/False/None, zfill) matches Python byte-for-byte.
Golden fixtures generated from the live Python modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ester, repo_finalize, github_pr

Extracted evaluated Python strings (line-continuation collapses
included) and machine-generated golden renders; exports
WorkspaceContextBlock for other packages. Local manifest model stubs
carry TODO(wiring) to swap for schemas types at wiring time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, scout

Byte-verbatim port; golden tests assert against base64-captured output
from the live Python prompt builders. Shared helpers
(WorkspaceContextBlock, FormatPriorUserResponses) exported for role
reasoners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verbatim port of retry_advisor, issue_advisor, replanner,
fix_generator, ci_fixer, pr_resolver, and fast planner prompts with
Python repr/truthiness/rune-slicing parity helpers; golden fixtures
rendered by the live Python builders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handler map keyed by exact Python reasoner names; qa_synthesizer uses
the direct AI path (not the harness) with the verbatim deterministic
fallback; notes, tool lists, guardrail application, and iteration_id
semantics preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation tester, finalize, PR

Seven handlers with verbatim notes/fallbacks, previous_error retry
context for git_init, per-issue WorkspaceInfo results, and fatal
propagation matching the Python try/except flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sprint planner

HITL-wrapped PM/scout via poll-based ask-user with budget 2; scout
stores scoped credentials and excludes them from its response; tech
lead writes plan/review.json; parse failures raise the exact Python
error strings except scout's deterministic fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
run_ci_watcher purely delegates to the cigate poller (no LLM); fixer
and resolver preserve tool lists, note formatting, and deterministic
fallbacks with empty collections serialized as [] for model_dump
parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retry advisor, issue advisor (HITL, ACCEPT_WITH_DEBT fallback),
replanner (HITL, inner parse-retry loop + raw log writes), issue
writer, verifier, and fix generator with verbatim fallbacks, notes,
and fatal propagation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Default path (coder->reviewer) and flagged path (coder -> QA||reviewer
-> synthesizer, errgroup barrier with gather(return_exceptions)
semantics), stuck detection over a 3-iteration window, iteration-state
checkpoints and per-iteration artifacts at the exact Python paths, and
memory read/write seams. Errors propagate only for fatal harness
failures or cancellation; all other terminals encode into IssueResult.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six-stage build orchestrator (git_init -> plan -> execute -> verify ->
finalize -> PR) with every stage routed through CallFn for DAG parity,
sequential task execution with per-task timeouts, deterministic
single-task planning fallback, and the seven delegating wrapper
registrations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full port of run_dag: level barriers with bounded concurrency,
per-issue timeouts, worktree setup/merge/integration-test/cleanup
gates, debt/split/replan gates with apply_replan level reset,
checkpoints at every Python save point, middle-loop issue advisor and
retry advisor, and multi-repo init. gather(return_exceptions)
semantics preserved at the barrier; race-clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full build pipeline: multi-repo clone with sparse checkout, git-init
retry loop, optional scout, plan/execute via control-plane calls,
verify+fix cycles, finalize, PR, CI-gate and approval-gate seams,
empty-build guard through the reasonerfail carrier, and deferred
scoped-credential cleanup. common.go carries the Deps/Handler surface
the sibling orchestrators build on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Thin wrapper building ExecutionConfig, wiring the unwrapping CallFn
and note function, forwarding resume/build_id/git_config/workspace
manifest options, and supporting external execute_fn_target coders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clone/fetch PR head with fallback, committer identity from env,
run_pr_resolver call, base-merge classification, CI gate via the
shared seam, and gh REST + GraphQL thread replies with resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PM -> optional scout -> architect/tech-lead bounded review loop with
auto-approve annotation -> sprint planner -> level computation and
sequence assignment -> parallel issue writers tolerant of individual
failures, with verbatim notes and artifact paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hax plan-approval revision loop on the poll-based approval client
(request_changes replans architect->tech lead->sprint planner, bounded
by max_plan_revision_iterations; rejected/expired terminal with
verbatim summaries), plus resume_build reconstructing plan_result from
the checkpoint and re-invoking execute with resume=true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports _run_ci_gate with verbatim terminal statuses and the
ci_fixer_model->coder_model resolution. Parity corrections against the
Python source: the startup-grace sleep stays at resolve()'s call site
(the gate never sleeps), and every watch keeps the original head_sha
anchor — Python never re-anchors to the fixer's commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas and others added 5 commits July 9, 2026 15:01
Registers the complete reasoner surface under exact Python names —
swe-planner: 5 orchestrators + 25 roles; swe-fast: 25 roles + fast
build/plan/execute/verify — with tags matching Python's registration
(router roles tagged, @app.reasoner orchestrators untagged), input
schemas for the orchestrator and fast reasoners, and the CI-gate /
approval-gate seams wired. Both binaries boot and register their
surfaces; whole-module build/vet/test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builder sparse-clones the agentfield SDK at a pinned AGENTFIELD_SDK_REF
build arg (cache-busts on ref change) so the replace directive resolves;
static swe-planner/swe-fast binaries; runtime mirrors the Python image's
tool surface plus the claude CLI. docker-compose.go.yml mirrors the
Python stack on the Go image; Python Dockerfile/compose untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without Config.PublicURL the SDK registers http://localhost:<port> as the
node's base URL, so a containerized control plane cannot route execute
calls back to the node (agent_unreachable 504). The Python SDK reads the
same AGENT_CALLBACK_URL env var (agent_server.py:758) before defaulting
to localhost; mirror it in BuildAgent. Found by the T7.2 functional
suite's deterministic run_ci_watcher call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
T7.2 / design §11(b): live-stack tests behind the functional build tag.
TestMain owns the compose lifecycle (up --build once, down -v always)
under a dedicated project name with host ports remapped to 18080/18003/
18004 so the stack coexists with anything on 8080/8003/8004; skips with
a message when Docker is unavailable.

Covered: /health on both nodes; exact reasoner-name registration parity
(swe-planner 30, swe-fast 29); CIWatchResult key-set parity via the
deterministic run_ci_watcher on both nodes; the ReasonerFailed carrier's
CP status contract (failed+result+error persist together, resultless
failed re-post does not clobber); env-gated (SWE_FUNCTIONAL_LLM=1)
minimal real build asserting BuildResult keys + planning-DAG child
reasoners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The authoritative documents the Go port was executed from: full Python
codebase inventory, Go SDK capability map and gap analysis, the module
design (schema/defaults strategy, concurrency mapping, HITL and
ReasonerFailed replication, checkpoint compatibility), and the wave
breakdown with per-task validation contracts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas and others added 2 commits July 9, 2026 17:23
Replaces the poll-based ApprovalClient workaround with the new SDK
Pause API (agentfield#744): a single Pauser interface threads through
the ask-user flow, planning/advisor roles, and the plan-approval gate;
the control plane's approval webhook now resumes executions instantly,
matching Python's app.pause semantics. Decision mapping and values
extraction unchanged. Requires the agentfield feat/go-sdk-parity SDK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also ignore local go.work files used for cross-repo dev builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AbirAbbas

Copy link
Copy Markdown
Collaborator Author

Update: SDK-native parity landed on this branch — the poll-based HITL workaround has been replaced with the Go SDK's new webhook-resumed Agent.Pause() (commit 2a47045), so approval resume is now instant, matching Python's app.pause exactly.

Merge-order dependency:

  1. Merge feat(sdk/go): Agent.Pause webhook approvals, harness cost reporting, incremental schema mode agentfield#744 first (Agent.Pause + harness cost_usd + incremental SchemaMode — all CI green).
  2. Then bump AGENTFIELD_SDK_REF in go/Dockerfile on this branch to the post-merge agentfield SHA (one line; cache-busts the SDK layer).
  3. feat(control-plane): af install support for Go agent nodes agentfield#745 (af install Go-node support, also green) is independent — it unlocks installing this node via af install with the Go manifest documented there.

Schema-mode and cost needed no SWE-AF changes: Python SWE-AF's effective schema mode is single-shot (never passes schema_mode; SDK default), which Go's unset default already matches, and Result.CostUSD flows through the harness layer automatically.

AbirAbbas and others added 18 commits July 9, 2026 17:39
agentfield#744 (Agent.Pause, harness cost, schema mode) and #745
(installer Go support) merged and released as v0.1.107 — the Go port
now builds against upstream main with no dev workspace shim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds go/test/mockclaude, a drop-in mock for the `claude` binary the harness
invokes, so the entire swe-planner pipeline can be exercised end-to-end with
ZERO LLM calls.

- Detects the reasoner role from the verbatim --system-prompt argv value
  (24 roles: planning, coding, advisor, ci, gitops) and constructs real typed
  structs from internal/schemas, guaranteeing schema validity.
- Performs the REAL git/gh work the gitops + coder roles expect (the Go port
  drives all git via the CLI agent): git_init branches, workspace_setup
  worktrees, per-issue coder commits, merger --no-ff merges, github_pr push +
  `gh pr create` (non-draft, per the github_pr prompt / private-repo support).
- Scenario engine (SWE_MOCK_SCENARIO override, flock-guarded per-role/issue
  counters and a JSONL invocation log under SWE_MOCK_STATE_DIR) whose default
  exercises: reviewer reject→approve (inner loop), reviewer block→issue_advisor
  retry_modified→approve (middle loop), verifier fail→fix→pass (outer loop),
  a needs_deeper_qa 4-call path, and a workflow-less repo → ci no_checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
go/test/e2e-fast/run.sh builds mockclaude as a `claude` shim, builds
swe-planner, starts (or reuses) `af server` on :18080 and the planner on
:18003 with the shim on PATH, resets the private GitHub fixture repo
swe-af-e2e-mock to a clean seed commit, POSTs an async swe-planner.build, polls
to terminal, and asserts: build succeeded, draft/PR created + files pushed, the
CP workflow DAG contains the expected reasoner set (incl. the flagged QA path,
issue_advisor and generate_fix_issues), the mock invocation log shows every
control-loop path fired, the CI gate reached no_checks, and notes were
recorded. ~1 min wall clock, zero LLM calls. `--keep` leaves the stack up;
runs/ is gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…thout CI

WatchPRChecks returned status="error" for any PR whose repo has no CI
workflows, so the documented no_checks gate verdict was unreachable in
practice. Two root causes in internal/cigate/watch.go:

1. The `gh pr checks --json` field list included `headSha`, which is not a
   field of `gh pr checks` in any gh version (its schema is bucket,completedAt,
   description,event,link,name,startedAt,state,workflow — headSha is a
   `gh pr view` concept). gh rejects an unknown --json field for the WHOLE
   call ("Unknown JSON field: headSha"), so every watch aborted immediately.
   Request only valid fields; headSHAOf() now always returns "", so the
   existing shaUnsupported fallback degrades SHA anchoring to PR-level verdicts
   (per-check SHA anchoring is not achievable via `gh pr checks`).

2. `gh pr checks` exits NON-ZERO with an empty stdout and a "no checks reported
   on the '<branch>' branch" stderr when a repo has no CI — the benign
   no_checks signal, not a failure. The error handler treated any non-zero
   exit with an empty body as status="error"; it now recognizes the
   "no checks reported" message and keeps polling so the no_checks verdict
   fires after the wait cap, while genuine gh failures still surface as error.

The prior TestNoChecksWhenPRHasNoCI stubbed `[]`+rc=0, a gh behavior that
never occurs, so it never caught this. Added TestNoChecksWhenGhReportsNoChecks
Reported (real behavior: rc=1, empty stdout, "no checks reported" stderr) and
TestGenuineGhErrorStillReported (a real failure must still be error).

Found via the go/test/e2e-fast harness: run_ci_watcher returned status="error"
(`Unknown JSON field: "headSha"`) against a workflow-less fixture repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reposition the Go port from a drop-in replacement to an opt-in sibling: the
Go nodes now default to distinct identities and ports so both stacks can
register against one control plane simultaneously.

  - swe-planner -> swe-planner-go, default PORT 8003 -> 8005
  - swe-fast    -> swe-fast-go,    default PORT 8004 -> 8006

NODE_ID / PORT env overrides still win — only the compiled defaults change.
Callers opt into the Go port via the -go reasoner path, e.g.
POST /api/v1/execute/async/swe-planner-go.build.

Changes:
  - cmd/swe-planner, cmd/swe-fast: BuildAgent default id/port.
  - node/register.go: registration tags swe-planner-go / swe-fast-go.
  - Empty-Deps fallbacks (orch/common, orch/approval_gate, dag/executor,
    fast/build) default to the -go identity; runtime call targets already
    derive from the live NodeID, so no hardcoded call targets exist.
  - Dockerfile EXPOSE/PORT/NODE_ID default to swe-planner-go:8005.
  - Makefile: unify AGENTFIELD_SDK_REF with the Dockerfile pin.
  - Unit-test fixtures updated to the -go identity. Python-parity comments
    that quote Python's actual "swe-planner"/"swe-fast" ids are left intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructure the Go compose from a standalone mirror of the Python stack into
an ADD-ON layered on top of the running Python stack, matching the opt-in
sibling model. It now defines ONLY the two Go nodes:

  - swe-agent-go -> node id swe-planner-go, :8005
  - swe-fast-go  -> node id swe-fast-go,    :8006

Run story: `docker compose up -d` (Python stack, owns the control plane) then
`docker compose -f docker-compose.go.yml up -d` (adds the Go nodes).

It is a separate compose project (name: swe-af-go) that joins the Python
stack's default network (external: swe-af_default) so control-plane:8080
resolves, and shares its workspaces volume (external: swe-af_workspaces).

build-db is dropped: nothing in go/ source consumes it. The DATABASE_URL_TEST
passthrough (used by target-repo integration tests) is kept, pointed at the
Python stack's build-db over the shared network. control-plane/build-db
depends_on is removed since they live in the Python project.

`docker compose -f docker-compose.go.yml config` validates. The go/Makefile
docker-up help is updated to describe the add-on precondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the zero-LLM e2e-fast harness and the black-box functional suite to
exercise the Go port's new opt-in-sibling identities.

e2e-fast (run.sh): NODE_ID=swe-planner-go on :18005, poll
/api/v1/nodes/swe-planner-go, and POST swe-planner-go.build — so the harness
drives the new default node id end to end. The built binary is still
cmd/swe-planner (only the runtime identity changed). Prompts/mockclaude are
untouched, so golden outputs stay byte-identical.

functional: the production docker-compose.go.yml is now an opt-in add-on with
no control plane of its own, so it can no longer be brought up in isolation.
Add a self-contained compose.functional.yml (control plane + both Go nodes +
build-db, own network/volumes) and repoint main_test.go at it. Its build
context / env_file use ../../.. so they still resolve to the repo root from the
file's new location under go/test/functional. Host-port override remaps to
18080/18005/18006; node-id constants and docstrings updated to
swe-planner-go / swe-fast-go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
go/README.md: update the binaries table, run targets, compose section, and
env-var table to the new -go identities/ports; add an "Opt-in alongside
Python" section explaining that Python remains the default (swe-planner :8003 /
swe-fast :8004) and the Go port registers separately as swe-planner-go :8005 /
swe-fast-go :8006 against one control plane. The compose section now documents
the add-on run story (Python stack first, then the Go nodes).

README.md: add ONE short "Go implementation (opt-in)" section near the end
pointing at go/README.md and stating the Python impl is the default with all
existing instructions unchanged. No other root README edits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate/role results

Python pydantic rejects a present-but-null value on every list field, so the
initial checkpoint (whose ~14 DAGState list fields are nil) and every role
harness result must serialise lists as [] not null. dict|None fields
(workspace_manifest, git_init_result, split_request, tests_passed) legitimately
stay null.

- schemas.EmptyForNilSlices: reflection walker replacing nil slices with empty
  ones, leaving maps and nil pointers untouched.
- DAGState.MarshalJSON: applies the walker at one chokepoint (checkpoint.json,
  BuildResult.dag_state embedding, replanner context).
- harnessx.Run: normalizes the parsed result (success + seeded-fallback paths).
- Tests: DAGState/checkpoint marshal has [] for all 14 list keys and null for
  workspace_manifest; walker leaves nil maps/pointers untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`go test -race ./...` was failing on two concurrent-execution test doubles:

- fast callScripter.call appends to calls from worker goroutines that
  callWithTimeout can leak past a timeout — guard with a mutex and read via
  snapshot()/count() in assertions.
- orch mockApp.Note is shared across the two concurrent Build() goroutines in
  TestBuildIsolationConcurrent — guard notes with a mutex.

Also gofmt the two files flagged by `gofmt -l`: fast/planner.go (import order)
and functional/reasoner_failed_test.go.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…K validation

The SDK now validates every harness output against the invopop-reflected schema
(harness/schema.go validateAgainstSchema). invopop's raw reflection over-rejects
valid output vs the source pydantic models, so schemaFor now post-processes each
schema to match pydantic exactly:

- enums: JSONSchemaExtend on AdvisorAction/IssueOutcome/ReplanAction/
  QASynthesisAction emits the `enum` constraint (invopop emitted bare strings),
  so an invalid action now fails validation and drives the retry loop.
- required: invopop marks every field required (no omitempty); schemas.
  MakePydanticFaithful replaces it with the pydantic no-default set per type
  (authoritative table cross-checked against defaults.go).
- additionalProperties: relaxed from invopop's `false` (pydantic ignores extras).
- nullable: Optional[...] fields (Go pointers) and map fields serialise as null
  when unset; their subschemas are made to accept null (a map cannot be
  distinguished from dict|None at the Go type level, so all maps accept null) —
  the minimal relaxation that never over-rejects.

Verified against the santhosh-tekuri validator the SDK uses: valid role outputs
(with null tests_passed/ask_user_form/agent_retro) pass; action:"banana" fails;
extra keys are accepted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chema validation

The SDK now validates harness output against the reflected schema. mockclaude
builds outputs by marshalling internal/schemas structs, whose nil slices marshal
to null and fail `type: array` validation — which would break e2e-fast. Run the
same schemas.EmptyForNilSlices normalization before writing so list fields are []
(nil pointers/maps stay null, accepted by the pydantic-faithful schema).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s for OpenRouter

The run_qa_synthesizer direct-LLM branch was unreachable: BuildAgent never set
cfg.AIConfig, so agent.AI() always returned "AI not configured" and only the
deterministic fallback ran.

- node.BuildAgent sets cfg.AIConfig = ai.DefaultConfig() when it validates (a
  key is present), guarded so a missing/invalid key leaves it nil and node
  startup still succeeds.
- coding.mapSynthModel maps the short role aliases (haiku/sonnet/opus) to
  anthropic/claude-*-4.5 / -4.1 ids when the client targets OpenRouter (its
  OpenAI-compatible endpoint rejects "haiku"); ids with "/" and the non-OpenRouter
  path pass through unchanged. Applied at the RunQASynthesizer AI call seam.
- Document the direct-LLM env vars (OPENAI/OPENROUTER key, AI_BASE_URL, AI_MODEL)
  in .env.example and both Go services in docker-compose.go.yml.
- Tests: resolveAIConfig with/without key, BuildAgent constructs without a key,
  mapSynthModel alias mapping on/off OpenRouter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrier

The SDK now exports a result-carrying agent.ReasonerFailed whose Result/ErrorDetails
are attached to the single 5x-retried status post by the async handler. Replace
SWE-AF's manual internal/reasonerfail carrier (POST status=failed then return a
plain error) with returning &agent.ReasonerFailed directly:

- build.go empty-build guard returns &agent.ReasonerFailed{Message, Result} with
  the BuildResult attached only when it is a JSON object (mirrors the retired
  buildBody guard); same message string, same preserved outcome.
- common.go drops the postFailedFn seam, PosterConfig, and the Deps Token/HTTPClient
  fields (only the carrier used them); AgentFieldServer stays for the approval base.
- delete internal/reasonerfail (nothing else uses it); update build_test.go and
  common_test.go to assert the *agent.ReasonerFailed contract; reframe the
  functional CP-contract test comments (the CP persistence guarantee is unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump AGENTFIELD_SDK_REF in the Dockerfile ARG default and the Makefile to the
pushed tip of agentfield feat/install-path-selector, which carries the SDK
changes SWE-AF's Go port now builds against (native ReasonerFailed, harness
JSON-schema validation, codex/opencode fixes). Changing the ref string busts the
sparse-clone layer cache so the new SDK is actually pulled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…K harness

The pinned SDK (45311737) validates harness output with
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1. Because SWE-AF/go builds the SDK
through the go.mod replace, that transitive dep must be recorded in SWE-AF's
go.mod/go.sum or `GOWORK=off go build` (CI/Docker) fails with a missing go.sum
entry. Added via `GOWORK=off go mod tidy`; version matches the SDK's go.mod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a `go` job to the CI workflow that reproduces the Docker sparse-clone of the
AgentField SDK at the pinned AGENTFIELD_SDK_REF, then runs gofmt, go build, go vet
and `go test -race` from SWE-AF/go with GOWORK=off (the same resolution CI/Docker
use). The existing python `test` job is unchanged. Repos are public so no clone
token is needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…outage resilience)

Real-LLM testing exposed that one timed-out status poll killed hour-long
builds when the control plane stalled under system load. The SDK now
retries transient poll failures with backoff (5-minute default window)
and only re-submits when the request provably never reached the server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant