Skip to content

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 33 commits into
mainfrom
release/pro-engine-optin
Open

Make installing the repo give you the Go node as swe-planner, with the bundled coding engine as an opt-in preview#122
AbirAbbas wants to merge 33 commits into
mainfrom
release/pro-engine-optin

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

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-planner

The 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 name swe-planner-go, so getting it meant knowing to type //go, and anyone who had already installed the Python swe-planner 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/SWE-AF//go

and the Go node drops its -go suffix — 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 declare swe-planner, af install https://github.com/Agent-Field/SWE-AF replaces an existing install in place: same node id, same triggers, node-scoped secrets untouched because the scope name never changes. swe-planner.build 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 swe_af 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.

Getting started is one key now

Several things quietly made the OpenRouter-only path fail:

  • .env.example shipped ANTHROPIC_API_KEY uncommented with a placeholder. Any non-empty value disables the OpenRouter auto-select, so the documented cp .env.example .env quick start forced the claude_code runtime with a bogus key. GH_TOKEN had the same problem — a placeholder made git/gh attempt (and fail) auth instead of degrading to the no-token path. Both are commented out now, with OpenRouter presented first.
  • Explicit SWE_DEFAULT_RUNTIME=open_code silently 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.
  • Fast mode had no OpenRouter detection at all — with only an OpenRouter key it resolved claude_code/haiku and could not run. It now uses the same detection as the main path, and honours the SWE_DEFAULT_MODELAI_MODELHARNESS_MODEL cascade it previously ignored.
  • Compose relied entirely on env_file: .env for provider keys on the planner services, so exporting a key in your shell gave you a working swe-fast and a keyless swe-planner. The functional compose file also still baked SWE_DEFAULT_RUNTIME=claude_code.

With only OPENROUTER_API_KEY set, the planner and fast nodes — Go and Python — now auto-select open_code and resolve every role to openrouter/deepseek/deepseek-v4-flash. GH_TOKEN stays optional. The claude_code path 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_execute registered, 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 install prompt), in .env.example, and in both READMEs. go/docs/pro-engine.md previously had zero inbound links.

Validation

  • gofmt, go build ./..., go vet ./..., go test -race -count=1 ./... — all clean
  • pytest — 1143 passed, 1 skipped
  • Verified against a live control plane: the engine registers as its own node, a code_task produced correct code, and the tests it wrote pass
  • Verified through a real af install: the engine resolves next to the built node binary, the sidecar spawns; with the execute bit removed it degrades cleanly instead of crash-looping
  • Verified the redirect end to end with a real af install against a local repo carrying it, with the Python node already installed: it warned, installed the successor as swe-planner with 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 with NODE_ID unset, registers as node_id: swe-planner, and the engine binary lands next to it at 0755
  • An earlier run of the same flow (pre-rename) reached a live control plane and completed an implement_issue with only OPENROUTER_API_KEY set

Note for reviewers

The engine binary is vendored at go/bin/swe-pro, refreshed to the engine's current main (15 commits past the previous build, including the in-process search fallback that removes the hard dependency on ripgrep being installed on the host). It is a static linux/amd64 build, 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

AbirAbbas and others added 7 commits August 3, 2026 15:20
…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>
@AbirAbbas AbirAbbas changed the title Ship the Go node with the bundled coding engine as an opt-in preview Make installing the repo give you the Go node, with the bundled coding engine as an opt-in preview Aug 4, 2026
@AbirAbbas AbirAbbas changed the title Make installing the repo give you the Go node, with the bundled coding engine as an opt-in preview Make installing the repo give you the Go node as swe-planner, with the bundled coding engine as an opt-in preview Aug 4, 2026
@AbirAbbas
AbirAbbas force-pushed the release/pro-engine-optin branch from 04109f8 to 7503b29 Compare August 4, 2026 16:34
AbirAbbas and others added 17 commits August 4, 2026 14:59
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
AbirAbbas force-pushed the release/pro-engine-optin branch from 7503b29 to b1a369a Compare August 4, 2026 19:01
AbirAbbas and others added 3 commits August 4, 2026 15:43
…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>
AbirAbbas and others added 2 commits August 4, 2026 16:04
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>
AbirAbbas and others added 2 commits August 4, 2026 17:18
…ct fix

Picks up swe-pro-go 9336a0b7a14c362a748e887e55b10f0b574279c5,
"fix(contract): separate the files a check lives in from the files it asserts
about" (swe-pro-go issue #22).

The engine's base contract check copies a contract's registered `paths` into a
detached checkout of the base commit so it can answer whether the bug
reproduced before the run started. `paths` means "the files the check lives
in", but nothing enforced that, and a coder that also listed its deliverable
had the deliverable copied into the base checkout — so the check passed there,
the engine concluded the work was already done, re-scoped the task to "report +
regression test, no behavioral change", then deadlocked on done-criteria
against the deliverable it had just cancelled. On a real bug fix the same path
discards the fix.

The contract schema now separates `asserted_paths` (what the check asserts
about — never copied, even when also listed in `paths`) from `paths` (the test
files — still copied), and the staleness adjudication is told whether each
copied file existed at base so a non-compliant contract is recognized rather
than believed.

Both binaries: CGO_ENABLED=0, -trimpath -ldflags="-s -w", mode 755.

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

Picks up swe-pro-go c361467fbebb300d3f52309bb29121a901a18266,
"fix(auditorgate): let a passing acceptance contract satisfy done-criteria
evidence" (swe-pro-go issue #23).

The engine's audit stage refused to trust a passing auditor verdict unless it
could see fresh test evidence, and it recognized test evidence only by matching
step-2 commands against ecosystem runners (`go test`, `pytest`, `npm test`, …).
A repo with no test framework gets the bespoke acceptance script the contract
protocol asks for instead, so the check saw no evidence, flipped a correct pass
to fail with "done-criteria evidence incomplete", and looped audit-fix cycles
across resume forever — no edit can make a shell script spell itself `pytest`.
Two runs ended that way with the requested file already correct on disk.

The registered acceptance contract now counts as that evidence. It is stronger
than the regex it replaces: the harness runs it itself each cycle, and the
base-contract check requires it to have failed at the base commit. Scoped to
the criteria check only, so a criterion the evidence genuinely does not meet
still fails the audit.

Both binaries: CGO_ENABLED=0, -trimpath -ldflags="-s -w", mode 755.

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

Picks up swe-pro-go 0cefff72f3d3b21703f335eed5e4e9669ad18f3d,
"fix(auditorgate): an empty diff is only a pass when the run delivered
something" (swe-pro-go issue #24).

The engine's audit gate skipped auditing and terminated a run as PASS on "no
changes to audit" without ever checking whether the run had produced anything.
A leaf whose coder wrote neither code nor an acceptance contract failed cycle 1
correctly, resumed, and then passed on that skip with no deliverable on disk.
An empty diff now passes only when an acceptance contract passes or an audit
already accepted the tree; otherwise it fails with an actionable verdict so the
audit-fix loop re-dispatches within its existing cycle and budget caps.

Both binaries: CGO_ENABLED=0, -trimpath -ldflags="-s -w", mode 755.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AbirAbbas added a commit to Agent-Field/agentfield that referenced this pull request Aug 4, 2026
…erseded-aware (#873)

* fix(packages): an install job reports the package the installer actually installed

`Manager.run` inferred the installed package by diffing the registry's names
before and after: whichever name is new must be the one this job installed.
That inference breaks on exactly the case `superseded_by` was added for.

A successor may declare its predecessor's name — an in-place rename, which is
what both Agent-Field/SWE-AF#122 and Agent-Field/pr-af#64 use, and what keeps a
node id, its triggers, and its node-scoped secrets intact across the swap. The
set of installed names is then identical before and after, so the diff finds
nothing and the job reports an empty package name. AgentField Desktop streams
that job's output, so the user watched a successful install end in
"install completed: " with the name missing.

When the successor's name *does* differ, the diff happened to work, but only by
luck: it returns the first registry name that is new, so any unrelated entry
appearing during the install is misattributed to this job.

The installer already knows the answer — `GitInstaller` tracks it in
`installedName` and propagates it through a redirect. Export it, thread it out
through the package service as `InstallPackageWithResult`, and have the job
prefer it, keeping the before/after diff as the fallback for installers that
cannot report a name. Node-dependency discovery used the same diff idiom and is
switched to the authoritative name too, which also stops it from walking the
dependencies of a package some other caller installed concurrently.

Updates take the authoritative name as well. `StartUpdate` pre-seeds the job
with the name being updated, so previously the installer's answer was
discarded — and an update whose recorded source redirects to a differently
named successor would then try to restart the package the redirect had just
uninstalled. It now reports and restarts the node that exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(desktop): name the node an install actually landed on

Every install result in the app was phrased from the request: the catalog row's
name, or the URL that was pasted. A `superseded_by:` redirect makes that a
guess — the manifest at the source hands the install off to a successor, which
may register under its own name.

Now that the control plane reports what it installed, repeat that instead:

  - a pasted repo says "pr-af installed" rather than "Installed from
    https://github.com/Agent-Field/pr-af", which is the more useful half of the
    sentence and the only one that tells you what to run next;
  - a catalog install names the successor if it ever disagrees with the row —
    the two agree for every entry today (that is the invariant catalog.ts
    documents), so a disagreement is drift worth seeing rather than hiding
    behind the row's own label;
  - an update that followed a rename reads "<old> replaced by <new>" instead of
    claiming it updated a node that no longer exists.

Each falls back to the previous wording when the control plane names nothing,
so an older control plane behaves exactly as it does today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(catalog): one PR-AF row, and install both consolidated nodes by repo

The catalog offered PR-AF twice — a Python row and a Go row shipping the same
reasoners under a name the user had to know to type. The two were
indistinguishable in the Install view except by the `-go` suffix, which is an
implementation detail leaking into a product list. Agent-Field/pr-af#64 collapses
them the way Agent-Field/SWE-AF#122 collapsed the SWE fleet: the root manifest
redirects to `//go`, and the Go node takes the product's name. So this is one
`pr-af` row, language go.

Both consolidated rows now install from the bare repo URL rather than naming
`//go` directly. Selecting the subdirectory would install the same node, but it
skips the redirect — and the redirect is the part that carries an existing
install across: it puts the successor in place first, migrates node-scoped
secrets, and only then retires the predecessor. Someone who already has the
Python node gets migrated by pressing Update; someone naming `//go` would only
collide with it. Naming the repo and letting the manifest decide is also simply
what a user can be told to type.

That changes the rule both catalogs are written against, so both header comments
now say the new one: an entry's `name` must equal the name the package ends up
REGISTERED under once the install settles, which under a redirect is not the
`name:` in the manifest at the source, and may live in a subdirectory the
catalog never mentions.

sec-af and cloudsecurity-af are untouched — neither ships a second
implementation, so neither has anything to collapse.

The SWE guard test generalizes to cover both repos: exactly one row per repo,
named for the product, sourced at the bare URL, language go, and the retired
implementation-suffixed name absent from the whole catalog — so a re-added row
fails here instead of quietly reappearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(skills): the PR review node is pr-af, not pr-af-go

The agentfield-use skill is what a harness reads to learn how to call the nodes
on this machine, and skillkit installs it into Claude Code, Codex, Cursor and
the rest — so its examples are the ids an agent will actually try. Its
`executions/active` sample still showed a run targeting `pr-af-go`, a name that
stops existing once Agent-Field/pr-af#64 lands.

Applied identically to the embedded copy under skillkit/skill_data so the two
stay byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(packages): pin that the production installer can report what it installed

The job reaches the authoritative name through a type assertion, and a failed
assertion is silent — it falls back to inferring the name from a registry diff,
which is exactly the path that returns nothing for an in-place `superseded_by`
replacement. Every other test in this file uses a stub that satisfies the
interface by construction, so none of them would notice a production wiring
change (a decorator, a swapped implementation) that quietly reverted the fix.

This one asserts against the service the server actually constructs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(packages): a node-dependency cycle must terminate

Switching dependency discovery from a registry snapshot to the authoritative
installed name dropped the only thing that stopped a cycle.

The snapshot version terminated by accident but reliably: the recursive call
received a snapshot that already contained the package just reinstalled, so the
second lap skipped it. Recursing on a single name removed that, and the
remaining guard — `depName != "" && isPackageInstalled(depName)` — cannot
substitute. It only knows a dependency's name for `af://registry/…` refs, and a
forced install reinstalls whatever is already there. Every update is forced
(`StartUpdate` → `startJob(JobUpdate, …, true)`), so two packages declaring each
other by bare git URL or local path recursed until the process died — with the
package-job manager's `active` latch held, blocking every later install.

Tracks the packages this install pass has walked instead, which does not depend
on ref form, on Force, or on registry state.

The accompanying suite pins the seam's behaviour end to end through the real git
installer rather than a stub: a redirect reports the successor — including when
the successor takes the predecessor's own name, the case a registry diff cannot
see and the reason this seam exists — a failed install reports no name at each
stage it can fail, an uninstallable dependency does not fail its parent, and a
cycle terminates. That last one fails in 30s against this fix reverted.

`manager_test.go` covers the other side: an installer that cannot report a name
still installs and falls back to the registry diff, so the old path stays intact
for anything that does not implement the newer seam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing fix

Picks up swe-pro-go 67d00765322d298eda4e48043d53d7f92d60143d,
"fix(specidentifiers): a file the spec names and the fix changed is not
missing".

The engine's spec-identifier gate required a filename quoted in an issue to
appear inside the text of the changed files. A source file never contains its
own name, so an issue whose "## Files — Modify: `ordinals.go`" line named the
target made the gate unsatisfiable: it reported the file missing while that
file was the one the fix changed, and re-dispatched a green tree until the run
ran out of wall clock. The changed-file list is now a fallback for the content
check, so naming a file and changing it satisfies the gate while a coder that
touched the wrong file is still caught.

Both binaries: CGO_ENABLED=0, -trimpath -ldflags="-s -w", mode 755.

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