Make installing the repo give you the Go node as swe-planner, with the bundled coding engine as an opt-in preview - #122
Open
AbirAbbas wants to merge 29 commits into
Open
Make installing the repo give you the Go node as swe-planner, with the bundled coding engine as an opt-in preview#122AbirAbbas wants to merge 29 commits into
AbirAbbas wants to merge 29 commits into
Conversation
…adapter internal/pro runs a prebuilt coding-engine binary as a supervised sidecar (SWE_PRO_ENGINE gate, env translation, crash backoff, parent-death cleanup) and adapts the execute_fn_target contract onto the engine's task interface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pro_execute registers on swe-planner-go only when the opt-in flag is set, so the default surface (and its Python-parity test) is unchanged. Adds a gated-surface test and pins the flag off in the exact-surface test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opt-in via SWE_PRO_ENGINE; the sidecar registers its own node on the same control plane and is stopped with a bounded wait when Run returns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SWE_PRO_MODELS_HIGH / SWE_PRO_MODELS_LOW / SWE_PRO_VARIANT forward the engine's sub-agent model pools and reasoning-effort variant per dispatch, alongside the existing SWE_PRO_MAX_COST. Unset keeps engine defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the opt-in flag set, build/execute requests that name no execute_fn_target route per-issue coding through pro_execute, with a per-run note; explicit targets always win and the default path is byte-identical with the flag unset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Startup log states the engine is enabled, how coding routes, and how to return to the classic engine; docs cover the opt-in surface and rollout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The black-box parity lists were not updated when implement_issue landed on both nodes: the live surface is 31/30, the test expected 30/29 and failed on main. Pre-existing; surfaced while verifying this branch's default surface is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 4, 2026
AbirAbbas
force-pushed
the
release/pro-engine-optin
branch
from
August 4, 2026 16:34
04109f8 to
7503b29
Compare
The image ships the engine at /usr/local/bin/swe-pro so an opted-in node finds it with no extra setup. It is inert unless SWE_PRO_ENGINE is set. Engine source c4f3fbe — requires a reachable control plane at startup. That build carries Agent-Field/swe-pro-go#19, which stops the engine attributing the commits it authors to a hardcoded internal identity. They now default to SWE-AF <swe-af@users.noreply.github.com> and follow SWE_AF_GIT_NAME / SWE_AF_GIT_EMAIL, matching the node's own commits. This only changed behaviour outside the container images: those export GIT_AUTHOR_* / GIT_COMMITTER_*, which git ranks above the engine's `-c user.name` overrides, so a host install was the exposed path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y runtime note Battle-testing the image surfaced two gaps: Python repos cannot produce test evidence in-container (no pytest), which fails evidence-based verification of correct work; and OpenRouter-only deployments inherit the compose default SWE_DEFAULT_RUNTIME=claude_code, silently degrading advisory/verification roles that then have no credential. Adds the test tooling to the image and documents the runtime setting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move GH_TOKEN from user_environment.required to optional in both node manifests (Python swe-planner and Go swe-planner-go): the af install / af run env gate no longer refuses to start without it. Builds on local or public repos run end-to-end with just an OpenRouter (or Anthropic) key — the PR phase already degrades to a logged note when push/PR auth is missing. GH_TOKEN is still offered at setup for private repos, pushing branches and opening pull requests. Docs updated to match, and the Go README now documents the runtime auto-select (open_code when only an OpenRouter key is present). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docker compose was baking SWE_DEFAULT_RUNTIME=claude_code whenever the var was unset, which broke OpenRouter-only deployments: the in-code auto-selection (open_code when only an OpenRouter key is present) never got a chance to run. Pass the empty string through instead — both the Python and Go nodes treat blank as unset. Also default the swe-fast secret vars to empty so compose does not warn about unset ANTHROPIC / CLAUDE_CODE_OAUTH_TOKEN / GH_TOKEN vars that are genuinely optional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follows the compose change that stopped forcing claude_code: the
expected line is SWE_DEFAULT_RUNTIME=${SWE_DEFAULT_RUNTIME:-} so the
in-code auto-selection (open_code when only an OpenRouter key is
present) decides the runtime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssing SWE_PRO_ENGINE=1 with no engine binary used to register pro_execute and default-route every issue to a sidecar node that never joins the control plane, burning every retry per issue. Registration and default routing now gate on pro.Available() (flag AND binary present), and ResolveBin() also searches next to the running executable — the layout an af install checkout produces (bin/swe-planner and bin/swe-pro side by side) — so the opt-in works outside the Docker image too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mode Three gaps broke the OpenRouter-key-only contract: - Explicitly setting SWE_DEFAULT_RUNTIME=open_code opted OUT of the auto-selected deepseek-v4-flash and silently fell back to minimax-m2.5. The runtime base and the auto default are now the same model, so opting in explicitly resolves identically. - Fast mode had no OpenRouter auto-detect: with only an OpenRouter key it resolved claude_code/haiku and could not run at all. DefaultFastRuntime now uses the same detection as the main path. - Fast mode ignored SWE_DEFAULT_MODEL / AI_MODEL / HARNESS_MODEL entirely. FastResolveModels now applies the same env cascade, below caller-supplied models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Python side of the same contract repair as the Go node, kept in lockstep: the open_code runtime base is now the shared OpenRouter default (so an explicit SWE_DEFAULT_RUNTIME=open_code no longer swaps the model), fast builds auto-select open_code when an OpenRouter key is the only credential, and fast model resolution honors the SWE_DEFAULT_MODEL / AI_MODEL / HARNESS_MODEL cascade below caller-supplied models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.env.example shipped ANTHROPIC_API_KEY and GH_TOKEN uncommented with placeholder values. Since any non-empty ANTHROPIC_API_KEY disables the OpenRouter auto-select, the documented `cp .env.example .env` quick start forced the claude_code runtime with a bogus key — and the GH_TOKEN placeholder made git and gh attempt (and fail) authentication instead of degrading to the no-token path. Both are now commented out, with OpenRouter presented first as the single key needed to get started. Also surfaces the coding-engine opt-in where users look for it: the go manifest gains SWE_PRO_ENGINE/VARIANT/MAX_COST, the root manifest gains the SWE_DEFAULT_RUNTIME knob it was missing next to SWE_DEFAULT_MODEL, and .env.example documents the engine env surface and warns that AI_MODEL also repoints every agent role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three gaps, all breaking the shell-exported-key workflow or the opt-in: - swe-agent (root and local compose) relied entirely on env_file: .env for provider keys, so a user who exported OPENROUTER_API_KEY in their shell got a working swe-fast but a keyless swe-planner. Both now pass the same keys through as swe-fast already did. - docker-compose.go.yml declared no SWE_PRO_* variables at all, so the only way to opt in was writing SWE_PRO_ENGINE into .env and relying on blanket env_file passthrough. It now passes the engine vars, defaulting the sidecar's public URL to a container-reachable address instead of the engine's own http://localhost:8801. - compose.functional.yml still baked SWE_DEFAULT_RUNTIME=claude_code, which the earlier unforcing sweep missed; it now passes through empty like the three deployment compose files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two statements went stale one commit after they were written. The doc said the compose files force SWE_DEFAULT_RUNTIME=claude_code (untrue since the unforcing commit) and told OpenRouter-only users to set SWE_DEFAULT_RUNTIME=open_code — advice that opts out of the auto-selected default and was silently downgrading the model. It also described SWE_PRO_PUBLIC_URL as 'derived' when nothing derives it; unset means the engine advertises localhost, which a control-plane container cannot reach. Also documents the missing-binary degradation and the binary search order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs still described the pre-auto-select world: Railway said to set two variables and presented GH_TOKEN as required, deployment.md listed Anthropic as the required key with open models as an 'alternative', and the runtime defaults table claimed open_code resolves to minimax-m2.5. All three contradict what the code now does. Also gives the opt-in coding engine a discovery path — go/docs/pro-engine.md previously had zero inbound links from anywhere — and documents the SWE_PRO_* knobs in the Go node's env table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os.Stat succeeds on a file that has lost its execute bit, so a binary copied without its mode looked available: pro_execute was registered and every issue was routed to an engine node whose process could never start, crash-looping on 'permission denied' until the supervisor gave up. That is strictly worse than not opting in. Availability now requires a regular file with an execute bit, so the not-runnable case takes the same graceful path as the missing case, and the warning names the offending path instead of the default one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fast mode repeats the model id rather than importing it (a module-scope import would be circular), so the two copies could diverge with every existing test still passing. Assert they are equal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine does an issue's coding in one long call, so a control plane that reaps by time-since-last-activity can report a healthy build as timed out while the engine is still working. Records the fix and the workaround for older control planes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Go node in go/ is the maintained SWE node — same reasoners, same interface, one static binary, and it ships the coding engine. Until now getting it meant knowing to append //go, and anyone who installed this repo earlier stayed on the Python node with no way across. Declare the root manifest superseded by the Go node, so `af install https://github.com/Agent-Field/SWE-AF` installs it and replaces an existing swe-planner. That URL is now the only thing a user has to know, before and after the switch. The Python node is unchanged and still what `python -m swe_af` and the Docker images run; the manifest stays here as the redirect. Requires an `af` that understands superseded_by — older ones simply install the Python node as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Go node carried -go suffixes on its package name, node ids, and reasoner tags. That existed so it could run beside the Python node on one control plane while the port was in progress; it is not something a user should ever see. This is the SWE node now, so it takes the product's names — the same ones the Python node uses and the same ones callers already have in their triggers. Nothing about a build changes when the implementation underneath it does. The package name matching the root manifest is deliberate: the root declares itself superseded_by this directory, so installing the repo replaces an existing swe-planner in place rather than leaving two half-equivalent SWE nodes installed side by side. Side-by-side is still available where it is genuinely useful — running both implementations against one control plane to compare them — but it is now an explicit NODE_ID override rather than the default, and docker-compose.go.yml is the one place that does it. The functional stack brings up its own control plane with no Python node on it, so it moves to the defaults like everything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AbirAbbas
force-pushed
the
release/pro-engine-optin
branch
from
August 4, 2026 19:01
7503b29 to
b1a369a
Compare
…exec
The vendored engine was a single linux/amd64 binary at go/bin/swe-pro, and
ResolveBin's sibling search looked for exactly that name. On macOS an
`af install` of this repo therefore put a Linux ELF next to swe-planner,
resolved it as the engine, and the supervisor crash-looped on
fork/exec .../bin/swe-pro: exec format error
until it gave up — with SWE_PRO_ENGINE=1 set and pro_execute registered,
because the binary was present and executable and so passed every gate.
macOS is where this node is developed and demoed, so the opt-in preview was
unreachable on the platform most likely to try it.
One repo has to serve both platforms, and there is no way to tell two builds
apart under one name. go/bin now carries swe-pro-darwin-arm64 and
swe-pro-linux-amd64, and the sibling search tries swe-pro-<GOOS>-<GOARCH>
first, falling back to plain swe-pro for layouts that place a single
hand-built engine beside the node. SWE_PRO_BIN stays authoritative and
/usr/local/bin/swe-pro stays the default: the image is one platform, so the
Dockerfile copies its own swe-pro-linux-amd64 to that path and nothing about
the container layout changes.
Both binaries are built with CGO_ENABLED=0 -trimpath -ldflags="-s -w" from
swe-pro-go 609ae34a ("fix(codeaf): build on darwin, where stat ctime is
named Ctimespec", the fix/darwin-build branch rebased onto main 93743e4,
which includes c4f3fbe — the rev the previous go/bin/swe-pro was built
from). That commit is the whole reason a darwin build exists: the engine
read the Linux-only Ctim field off syscall.Stat_t and would not compile for
GOOS=darwin.
bin/ is now a versioned directory, so go/.gitignore ignores the unsuffixed
swe-planner / swe-fast / swe-pro names a local build or the installer's build
step drops there, and the suffixed engine builds stay tracked.
Verified on macOS arm64 end to end: af install from this branch, control
plane + node up, SWE_PRO_ENGINE=1 spawns the darwin sidecar, engine node
swe-pro registers (code_task/code_resume), and a capped code_task produced
and committed the requested file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An OpenRouter-only install resolves every role model to config.openRouterAutoDefaultModel = "openrouter/deepseek/deepseek-v4-flash". That id is correct — it is LiteLLM-style, which is the currency of this repo's config and exactly what the open_code harness runtime wants. But run_qa_synthesizer does not go through a harness: it calls the direct client (Deps.AI), which talks to OpenRouter's OpenAI-compatible endpoint, and that API names the same model "deepseek/deepseek-v4-flash". Handed the routing prefix it returns a 400. mapSynthModel passed any id containing "/" straight through, so the prefix survived to the wire. The synthesizer then took its "any failure" branch and returned the deterministic fallback — Synthesizer failed — defaulting to FIX. QA passed=True, review approved=True. — which reads like a QA verdict but is really a swallowed transport error. Every iteration on an OpenRouter-only install was being pushed back to FIX with no model ever consulted. Translate at the boundary that owns the wire format: when the direct client targets OpenRouter, strip a leading "openrouter/". openRouterAutoDefaultModel is untouched — the harness path needs the prefix and is not affected — and the strip is conditional on IsOpenRouter() so a LiteLLM-style proxy behind AI_BASE_URL still receives the prefix it routes on. run_qa_synthesizer remains the only direct-LLM consumer: deps.AI.AI( and ai.WithModel each appear exactly once in the module, both in this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product decision: the engine is the coding path users should get, so an
`af install` node should run it without anyone reading a doc first. Flipping
the polarity does not need a code change — the af env resolver resolves each
declared user_environment var as process env → node secret → global secret →
manifest default, then appends the result to the node process env. Declaring
default: "1"
on the existing optional SWE_PRO_ENGINE entry is therefore enough: installed
nodes get the engine, and because the process env still wins,
`SWE_PRO_ENGINE=0 af run swe-planner` still opts out. The entry stays optional
so nothing prompts.
pro.Enabled() already whitelists "1"/"true"/"yes"/"on" and treats everything
else as off, so "0" and "false" disable it as required — no parsing change.
That contract now matters far more than it did (a value is always present on an
installed node, so opting out can only mean writing a falsy one), so
TestEnabledOptOutContract pins all four corners including a genuinely unset
variable, which must stay off: a bare binary launched with no manifest has no
business starting a sidecar.
Wording follows the new reality. The startup line no longer calls this a
preview or promises a future default:
pro engine enabled: engine node "swe-pro" joins the control plane; builds
route per-issue coding through it. Set SWE_PRO_ENGINE=0 to use the classic
coding loop instead.
The per-run routing note, the pro_execute description, docs/pro-engine.md,
go/README.md and the .env.example block get the same treatment, and
docker-compose.go.yml defaults to ${SWE_PRO_ENGINE:-1} to match the manifest.
The Dockerfile deliberately sets no ENV: the manifest and compose are the
controlled runtime surfaces, and an image that hardcoded the flag would take
the choice away from whatever runs it.
Also de-flakes TestSupervisorStopsOnCancel, which failed this change's
acceptance run twice. It slept a fixed 300ms before cancelling, so under
whole-module load it could cancel before the sidecar had printed, and it read a
strings.Builder that the pipeLines goroutines were concurrently writing — a
real data race, not just a timing guess. It now polls a mutex-guarded buffer
until the line lands. `go test ./... -count=1` and `go test ./internal/pro/
-race -count=2` are both clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The on-by-default decision is scoped to `af install`, where the manifest injects SWE_PRO_ENGINE=1 and `af secrets`/env give the user an opt-out. A checkout deployed with docker compose is production infrastructure someone configured deliberately; defaulting the engine on there changes running systems on a pull. Compose goes back to passing SWE_PRO_ENGINE through unset, which the binary treats as off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cation fix Picks up swe-pro-go 7cab26990feb16133cfce6ec1bee37b6fb34e6e8, "fix(fullverification): stop demanding a test suite from a workspace with no project in it". The engine's full-project verification gate demanded a test entrypoint from every workspace, while the sibling build gate had always been conditional. On a repository with no project in it — a `git init` carrying a README, which is exactly what a first `code_task` against a fresh directory looks like — both demands are unsatisfiable, so the acceptance contract would pass, verification would fail on "no standard test entrypoint was discoverable", and the audit-fix loop plus the auto-resume supervisor would repair that non-defect until the run hit its cost ceiling. The deliverable was already written and committed seconds in. Both gates now stand down for a workspace carrying no language manifest, no build system and no test suite, and the verification section says outright that it proved nothing so the audit stage is not left reading a vacuous pass as a green one. Anything that looks like a project is held to the same floor as before, undiscoverable test command included. Both binaries: CGO_ENABLED=0, -trimpath -ldflags="-s -w", mode 755. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes
af install <this repo>give you the Go node, makes an OpenRouter key the only thing you need to get it working, and ships the Go node's bundled high-performance coding engine behind an opt-in flag.Installing the repo gives you the Go node — as
swe-plannerThe Go node is the maintained SWE node — same reasoners, same interface, one static binary, and it ships the coding engine. But it lived in
go/under the nameswe-planner-go, so getting it meant knowing to type//go, and anyone who had already installed the Pythonswe-plannerstayed on it forever.Two changes make the bare repo URL the whole story. The root manifest carries a redirect:
and the Go node drops its
-gosuffix — package name, node ids, and reasoner tags — to take the product's names. 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 declareswe-planner,af install https://github.com/Agent-Field/SWE-AFreplaces an existing install in place: same node id, same triggers, node-scoped secrets untouched because the scope name never changes.swe-planner.buildmeans 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 swe_afanddocker-compose.ymlrun. Because the two now answer to the same ids, running both against one control plane needs an explicitNODE_IDon one of them —docker-compose.go.ymldoes exactly that, and is now the only place carrying-goids. 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.Getting started is one key now
Several things quietly made the OpenRouter-only path fail:
.env.exampleshippedANTHROPIC_API_KEYuncommented with a placeholder. Any non-empty value disables the OpenRouter auto-select, so the documentedcp .env.example .envquick start forced theclaude_coderuntime with a bogus key.GH_TOKENhad the same problem — a placeholder madegit/ghattempt (and fail) auth instead of degrading to the no-token path. Both are commented out now, with OpenRouter presented first.SWE_DEFAULT_RUNTIME=open_codesilently swapped the model. It opted out of the auto-selected default and fell back to minimax. The runtime base and the auto default are now the same model, so opting in explicitly resolves identically.claude_code/haikuand could not run. It now uses the same detection as the main path, and honours theSWE_DEFAULT_MODEL→AI_MODEL→HARNESS_MODELcascade it previously ignored.env_file: .envfor provider keys on the planner services, so exporting a key in your shell gave you a workingswe-fastand a keylessswe-planner. The functional compose file also still bakedSWE_DEFAULT_RUNTIME=claude_code.With only
OPENROUTER_API_KEYset, the planner and fast nodes — Go and Python — now auto-selectopen_codeand resolve every role toopenrouter/deepseek/deepseek-v4-flash.GH_TOKENstays optional. Theclaude_codepath is unchanged.Coding engine (opt-in preview)
The Go node bundles a prebuilt engine that is inert unless
SWE_PRO_ENGINE=1. With it set, builds route per-issue coding through it; unset it and you are back on the classic loop. Branch, push and PR creation stay with the standard pipeline.If the flag is on but no runnable binary is found, the node logs a warning and comes up on the classic loop. That check is deliberately about runnability, not mere existence: a binary that lost its execute bit used to look available, get
pro_executeregistered, and route every issue to an engine that could never start — worse than not opting in.The opt-in is now discoverable: it is declared in the Go node's manifest (so it appears in the
af installprompt), in.env.example, and in both READMEs.go/docs/pro-engine.mdpreviously had zero inbound links.Validation
gofmt,go build ./...,go vet ./...,go test -race -count=1 ./...— all cleanpytest— 1143 passed, 1 skippedcode_taskproduced correct code, and the tests it wrote passaf install: the engine resolves next to the built node binary, the sidecar spawns; with the execute bit removed it degrades cleanly instead of crash-loopingaf installagainst a local repo carrying it, with the Python node already installed: it warned, installed the successor asswe-plannerwith its source recorded as…//go, replaced the predecessor's files in place, and left both the node-scoped and global secrets intact. The installed binary, started withNODE_IDunset, registers asnode_id: swe-planner, and the engine binary lands next to it at0755implement_issuewith onlyOPENROUTER_API_KEYsetNote for reviewers
The engine binary is vendored at
go/bin/swe-pro, refreshed to the engine's currentmain(15 commits past the previous build, including the in-process search fallback that removes the hard dependency onripgrepbeing installed on the host). It is a staticlinux/amd64build, reproducible — building it twice gives byte-identical output.The branch is laid out so that binary is added exactly once in history: the refresh amends the commit that introduces it rather than layering a second 20 MB blob, so the branch still carries one.
🤖 Generated with Claude Code