Skip to content

Offer a single SWE node, and let a manifest declare itself superseded - #864

Merged
AbirAbbas merged 10 commits into
mainfrom
feat/swe-catalog-single-go-entry
Aug 4, 2026
Merged

Offer a single SWE node, and let a manifest declare itself superseded#864
AbirAbbas merged 10 commits into
mainfrom
feat/swe-catalog-single-go-entry

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The catalog advertised two SWE nodes — the Python swe-planner at the repo root and swe-planner-go under //go. Installing SWE should give you one thing, so this collapses them to a single entry named swe-planner, and adds the mechanism that carries an existing install across — the open question the first version of this PR flagged and left unanswered.

One catalog entry

  • control-plane/internal/cli/catalog.go — drop the Python entry; the surviving one keeps the fuller product description and is named swe-planner, after the product rather than the implementation. Catalog goes 6 → 5 entries.
  • desktop/src/shared/catalog.ts — same collapse in the app's curated list. The two lists have no automated cross-check, so they are edited in lockstep; the desktop card keeps its own wording.
  • Docs and skills — the README install command is now the bare repo URL (the manifest redirects), and every example uses the swe-planner node id, so a first-time user's first command after a catalog install actually works. docs/mcp-integration.md, skills/agentfield-use/SKILL.md and its byte-identical skillkit/skill_data mirror updated to match.

superseded_by — a package retires itself, without the control plane knowing who it is

Collapsing the catalog on its own strands anyone who already installed the old node: the entry disappears, and Desktop's update/uninstall path refuses names it cannot find in the catalog. Fixing that must not teach AgentField about any particular node, so the redirect is an optional manifest key rather than a table of names in here:

name: swe-planner
superseded_by: https://github.com/Agent-Field/SWE-AF//go

The value is any installable source af install accepts, including a //subdir selector and an @ref. Installing a superseded package installs the successor instead and replaces the old one. Any node author can rename or replace their own node this way; the control plane stays agnostic, and the product-specific data lives in the product's own repo. Documented in docs/installing-agent-nodes.md — a generic key nobody can find is not much use.

What the user sees when the superseded node is installed — an explicit warning before anything is touched:

⚠️  swe-planner has been superseded by https://github.com/Agent-Field/SWE-AF//go
⚠️  swe-planner is currently installed and WILL BE REPLACED. The successor is installed
    first and node-scoped secrets are carried over; if that fails, what you
    have now is left as it is.
  installing the successor instead

The ordering is the whole design, and each rule has a test:

  • The redirect is taken before the force check and before anything is copied, so a redirect never half-installs the package it is redirecting away from.
  • The successor installs first, and a failure leaves what the user had exactly as it was — see the safety net below, which is what makes that true when the successor shares the name.
  • A successor may carry the same name as its predecessor — a node renaming itself in place. The redirect carries the user's consent into that install rather than tripping the successor's own "already installed (use --force)" check. Nothing needs retiring afterwards and node-scoped secrets are already in the right scope.
  • Where the names do differ, node-scoped secrets move before the uninstall — UninstallPackage deletes that scope outright, so every af secrets set KEY --node <old> value would otherwise vanish in the swap. A value already set on the successor wins: the user set that one deliberately, and later. Global secrets are shared and untouched.
  • Retiring never fails the install. The successor is already working, so a stubborn leftover prints how to remove it by hand instead of reporting failure.
  • Chains are bounded at 3 hops, so two manifests pointing at each other fail loudly instead of cloning forever.

The redirect is a git-install behaviour only. A local-path install ignores it, which is the documented way to install a superseded node on purpose.

Safety net: a failed reinstall no longer eats the package it was replacing

copyPackage clears the destination before the replacement is copied, and long before its dependencies build. Replacing a package in place therefore had a window where a failure in the dependency step — a missing Go toolchain is enough, and that is precisely the machine most likely to hit it — left the user with neither the node they had nor a working new one. The existing directory is now set aside and put back on any failure before the registry is updated.

This is not specific to the redirect: plain af install --force over any installed package had the same hole. The test asserts the predecessor's own files are back on disk after a deliberately failing install, and fails without the fix.

Bug found alongside: the //subdir selector was dropped from the recorded source

updateRegistryWithGit recorded info.URL — the URL after ParseGitURL has split the subdirectory off. That was invisible for the //go selector form, whose caller re-parses the original string, but not when the subdirectory arrives via the --path flag or the install API, both of which pass it alongside a bare URL. Those installs recorded a repo root as their source, so updating a //go-installed node would reinstall the repo root instead — the exact inverse of what this PR exists to do. Kept as its own commit so it can be reviewed on its own terms.

Tests

git_supersede_test.go — redirect to successor; replace an existing install; same-name replace-in-place (registry, files, and the absence of an "already installed" error); secrets surviving a same-name replace; secret migration across a renaming replace, including successor-wins and globals-survive; the no-prior-install case; the bounded cycle; restore-after-a-failed-reinstall; the stash/restore/discard unit; appendSubdirSelector round-tripping back through ParseGitURL; and the --path source recording.

The two behavioural fixes were checked by reverting each and confirming the corresponding test goes red — the restore test reports the predecessor's files simply missing, which is the data loss it exists to prevent.

The pre-existing catalog assertion was Contains(out, "swe-planner"), which "swe-planner-go" also satisfies as a substring — it would not have caught a mistyped name or a re-added Python entry. Both catalog tests now pin the surviving row's name rather than only asserting the other one is absent.

Validation

GOFLAGS=-buildvcs=false go build ./..., go vet ./..., go test ./... (51 packages), and npm ci / npm run typecheck / npm test (387 tests) in desktop/ — all clean.

Verified end to end with a real af install against a local repo carrying the redirect, with the Python node already installed and holding one node-scoped secret:

  • warned, then installed the successor as swe-planner — no --force, no error
  • the predecessor's files are gone and the successor's are in their place, under the one directory
  • the registry holds a single swe-planner entry with its source recorded as …//go, so the next update resolves the same package
  • the node-scoped secret and the global one both survived
  • no backup residue left in packages/
  • the installed node binary, started with NODE_ID unset, registers as node_id: swe-planner

Pairs with Agent-Field/SWE-AF#122, which puts the superseded_by line in the SWE-AF root manifest and renames the node.

🤖 Generated with Claude Code

@AbirAbbas
AbirAbbas requested a review from a team as a code owner August 3, 2026 19:33
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.00% 87.40% ↓ -0.40 pp 🟡
sdk-go 92.70% 92.00% ↑ +0.70 pp 🟢
sdk-python 93.82% 93.73% ↑ +0.09 pp 🟢
sdk-typescript 91.05% 90.42% ↑ +0.63 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.59% 85.75% ↓ -0.16 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 146 83.00%
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening the catalog and docs. I don’t think we should merge this as-is because it knowingly strands existing desktop users who already installed : after they upgrade, the app loses the update/uninstall path for that node. Please add a migration path or compatibility handling for the old install before we collapse the catalog entry.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tightening the catalog and docs. I do not think we should merge this as-is because it knowingly strands existing desktop users who already installed swe-planner: after they upgrade, the app loses the update/uninstall path for that node. Please add a migration path or compatibility handling for the old install before we collapse the catalog entry.

AbirAbbas and others added 7 commits August 4, 2026 10:54
`af catalog` listed the SWE fleet twice — a root Python node and its Go
counterpart — which forced a harness to pick between two rows that ship the
same reasoners. Keep only the Go node (installed via the `//go` source
selector) and give it the full fleet description.

Tighten the pretty-output assertion to `swe-planner-go` (the old
`swe-planner` substring matched either row) and add a guard test that pins
the invariant: exactly one entry installs from Agent-Field/SWE-AF, its
source ends in `//go`, and no entry is named exactly `swe-planner`, so a
re-added root entry fails loudly instead of quietly reappearing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the `af catalog` change: the Install view listed the SWE fleet twice
with identical copy, so the two rows were indistinguishable to a user. Keep
only the Go node sourced from `//go`, with the same description wording the
CLI catalog now uses.

Add a test pinning the invariant — `swe-planner-go` is present, its source
ends in `//go`, exactly one entry installs from Agent-Field/SWE-AF, and no
entry is named exactly `swe-planner`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catalog now offers the SWE fleet as a single Go node, so the docs that
still told users to install the bare repo root and call `swe-planner` were
advertising a node the catalog no longer lists. Update the README quickstart
to `af install …/SWE-AF//go` plus `af run`/`af call swe-planner-go`, and
switch the MCP example flow and the agentfield-use skill examples to the same
node id.

The `--path go` examples in installing-agent-nodes.md stay as they are — they
document the subdirectory selector itself; only the surrounding framing is
reworded so the Go node reads as the advertised install rather than a port of
the root node.

The skill edit is applied identically to the embedded copy under
internal/skillkit/skill_data so the two stay byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catalog collapse rewrote this card's copy to match the CLI catalog's
phrasing, which reads out of place next to the other entries in this file.
Restore the original line — it describes the surviving Go node just as
accurately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A subdirectory can reach the installer two ways: the `//subdir` selector on
the URL, or the --path flag. The install API takes the second route — it
splits the selector off the URL and passes it as an option — so info.URL
arrives bare and the registry records the REPO ROOT as the source.

The next update resolves that bare source and installs whatever manifest lives
at the repo root, which is a different package than the one installed. For a
repo shipping a Python root and a Go port side by side, updating the Go node
silently replaces it with the Python one.

Put the selector back when it came from the flag, so the recorded source
round-trips through ParseGitURL.

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

A node author who renames or replaces their own node has no way to carry
existing users across: `af install <their url>` keeps installing the old
package forever, because the manifest at that source is the only thing the
installer looks at.

Add an optional `superseded_by:` key naming an installable source. Installing
a superseded package installs the successor instead, and replaces the old one
when it is already present. The redirect lives in the package's own manifest,
so the control plane needs no knowledge of any particular node — any author
gets this, and no catalog or table here has to name them.

Ordering and safety:
  - The successor is installed FIRST; only then is the old package retired, so
    a failed install leaves the user's existing node exactly as it was.
  - The redirect is taken before the force check and before anything is
    copied, so it never half-installs the package it redirects away from.
  - Node-scoped secrets move to the successor before the old package is
    uninstalled, which would otherwise delete that scope outright. Values
    already set on the successor win. Global secrets are shared and untouched.
  - Retiring the old package never fails the install: the successor is already
    working, so a leftover is a cleanup chore, not a failure.
  - A chain is bounded at 3 hops so two manifests pointing at each other fail
    loudly instead of cloning forever.

The user is warned before the swap, naming what will be replaced.

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

One test per behaviour, driven through the real InstallFromGit against the
existing fake-git harness rather than against the internals:

  - a superseded package installs its successor, and its own name never
    reaches the registry
  - an already-installed superseded package is replaced: successor present,
    old entry and old package directory gone
  - node-scoped secrets follow the swap, a value already set on the successor
    wins, and global secrets are untouched
  - with nothing to replace it is a plain install, no error
  - two manifests pointing at each other fail with a bounded-chain error and
    install nothing
  - a recorded --path source round-trips through ParseGitURL back to the same
    repo AND subdir

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas force-pushed the feat/swe-catalog-single-go-entry branch from 239e1c1 to 7c43de9 Compare August 4, 2026 15:08
@AbirAbbas AbirAbbas changed the title Offer a single SWE node in the install catalog Offer a single SWE node, and let a manifest declare itself superseded Aug 4, 2026
AbirAbbas and others added 3 commits August 4, 2026 11:36
A node that renames itself takes the name its predecessor held, and
`superseded_by` could not express that: the redirect installed the
successor without carrying the user's consent to replace, so the
successor's own force check rejected it with "already installed (use
--force to reinstall)". The redirect has already printed an explicit
replacement warning by then, so it now carries that consent through.
Same name means there is nothing to retire afterwards and node-scoped
secrets are already in the right scope, both of which the existing
short-circuit handles.

That makes the failure mode worse, though, and this fixes it too:
copyPackage clears the destination before the replacement is copied,
and long before its dependencies build. A replace that dies in the
dependency step — a missing toolchain is enough — used to leave the
user with neither the package they had nor a working new one. The
existing directory is now set aside first and put back on any failure
before the registry is updated, which also covers a plain
`af install --force` on any package.

Also restores the doc comment on updateRegistryWithGit, which an
earlier commit in this branch left attached to the wrong function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The catalog named it swe-planner-go, after the implementation. That was
only ever a workaround for the two SWE manifests needing distinct
registry keys, and the node now declares itself swe-planner — a name
that survives the implementation changing under it, and the one its
triggers already use. Catalog `name` must equal the manifest `name`, so
this follows rather than leads.

The install command in the README drops the `//go` selector too: the
root manifest redirects, so the bare repo URL is the whole instruction.

Both catalog tests keep their guard against a second SWE row, and now
pin the surviving row's name rather than merely asserting the Python one
is absent — the assertion that would have caught this rename going
half-done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The key shipped undocumented, which defeats the point of making it
generic — a node author cannot use a manifest field they cannot find.
Documents what it accepts, and the ordering guarantees that make a
redirect safe to run against an installed node: resolved before
anything is copied, successor installed first, node-scoped secrets
carried across, retiring never fails the install, chains bounded.

Also corrects the claim just above it that a root node and a `--path`
node from one repo always coexist. They coexist when their names
differ, and replace each other when they do not — which is exactly
what SWE-AF, the example named there, now does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AbirAbbas
AbirAbbas merged commit f4acec1 into main Aug 4, 2026
33 checks passed
@AbirAbbas
AbirAbbas deleted the feat/swe-catalog-single-go-entry branch August 4, 2026 18:26
AbirAbbas added a commit to Agent-Field/pr-af that referenced this pull request Aug 4, 2026
* feat(packages): installing this repo gives you the Go node, as pr-af

The Go node is the maintained PR review node — same reasoners, same
interface, one static binary, no per-node venv to build. But it lived in
`go/` under the name `pr-af-go`, so getting it meant knowing to type
`//go`, and anyone who had already installed the Python `pr-af` stayed on
it forever.

Two changes make the bare repo URL the whole story. The root manifest
carries a redirect:

    superseded_by: https://github.com/Agent-Field/pr-af//go

and the Go node drops its `-go` suffix to take the product's name. The
suffix existed so the port could run beside the Python node during the
migration; it is not something a user should ever have had to type.

Since both manifests now declare `pr-af`, `af install
https://github.com/Agent-Field/pr-af` replaces an existing install in
place: same node id, same triggers, node-scoped secrets untouched because
the scope name never changes. `pr-af.review` means the same thing before
and after, whichever implementation is serving it.

The mechanism behind the redirect is a generic manifest key in AgentField
(Agent-Field/agentfield#864) — including the part that lets a successor
share its predecessor's name, which is what an in-place rename needs. This
repo only supplies the data.

The Python package is untouched and still what `python -m pr_af.app` and
docker-compose.yml run. Because the two now answer to the same ids,
running both against one control plane needs an explicit NODE_ID on one of
them — docker-compose.go.yml does exactly that, and is now the only place
carrying `-go` ids. To install the Python node deliberately, clone the
repo and install the checkout as a local path; local-path installs do not
follow the redirect.

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

* refactor(go): the node's own identity is pr-af, not pr-af-go

Follows the manifest rename through the code that carries the node's
runtime identity: the NODE_ID default, the port doc comments, the package
docs, and every test that asserted the old default. Callers now reach
`pr-af.review`, matching what the manifest registers.

The 16 semantic role tags are unchanged — they were never node-identity
tags, and the comment that explained the distinction by contrast with
SWE-AF's `-go` role tags no longer has anything to contrast with.

`-go` survives in exactly one place: docker-compose.go.yml, which runs the
Go node beside the Python one against a single control plane and therefore
has to override NODE_ID to keep the two distinguishable. The functional
compose stack does not — it runs the Go node alone, so it uses the real id.

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

* docs: the install story is the bare repo URL

Both READMEs still told the reader the Python node was the default and
that the Go node was an opt-in sibling reached by typing `-go` into the
reasoner path. Neither is true once the root manifest redirects: `af
install https://github.com/Agent-Field/pr-af` gives you the Go node as
`pr-af`, and an existing Python install is replaced in place.

Documents the local-path escape hatch in both files, since that is now the
only way to install the Python node deliberately, and keeps the Go add-on
Compose file described as what it is — the way to run both at once during
a changeover, which is why it still carries `-go` ids.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <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.

2 participants