Skip to content

Installing this repo gives you the Go node, as pr-af - #64

Merged
AbirAbbas merged 3 commits into
mainfrom
feat/consolidate-go-as-pr-af
Aug 4, 2026
Merged

Installing this repo gives you the Go node, as pr-af#64
AbirAbbas merged 3 commits into
mainfrom
feat/consolidate-go-as-pr-af

Conversation

@AbirAbbas

Copy link
Copy Markdown
Contributor

Makes af install https://github.com/Agent-Field/pr-af give you the Go node, registered as pr-af. Mirrors what Agent-Field/SWE-AF#122 does for the SWE fleet.

The problem

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.

The change

The root manifest carries a redirect:

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

and the Go node drops its -go suffix — package name and node id — 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, installing the repo 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.

What is deliberately unchanged

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. Both READMEs document that escape hatch.

The 16 semantic role tags are unchanged — they were never node-identity tags.

Validation

Ran the literal steps from .github/workflows/ci.yml:

  • ruff check src/ scripts/ — all checks passed
  • docker build -t pr-af:test . — passed
  • go build ./..., go vet ./..., go test ./... — all clean
  • test -z "$(gofmt -l .)" — clean
  • pytest — 73 passed
  • Both manifests re-parsed to confirm the resulting shape: root pr-af + superseded_by//go; go/ pr-af, language: go, node_id: pr-af, port 8007

Merge order

Nothing here depends on another PR. The desktop/CLI catalog change on the AgentField side points its PR-AF row at this bare repo URL and should land after this.

🤖 Generated with Claude Code

AbirAbbas and others added 3 commits August 4, 2026 16:00
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>
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>
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>
@AbirAbbas

Copy link
Copy Markdown
Contributor Author

Heads-up on sequencing, from testing this against a real `af` built from agentfield `main`.

Selecting a subdirectory bypasses the redirect: `resolvePackageRoot` resolves `//go` and only `go/agentfield-package.yaml` is parsed, so the root manifest's `superseded_by` is never read. That means renaming this manifest to `pr-af` breaks the `pr-af-go` catalog row that shipped clients already carry (it is in `v0.1.120`, sourced at `.../pr-af//go`).

Verified with a local repo fixture carrying this PR's manifests:

  • the row labelled `pr-af-go` installs a package registered as `pr-af`, with no redirect warning — its card never flips to Installed, the other row's does instead;
  • a user who already has the Python `pr-af` gets `package pr-af already installed (use --force to reinstall)` — a click that works today;
  • `af run pr-af-go` fails with `agent node pr-af-go not installed`.

Nothing in this PR is wrong — this is inherent to the rename, and no merge order protects binaries already on disk. But merging Agent-Field/agentfield#873 and cutting a release first removes the stale `pr-af-go` row from subsequent builds, so only users who have not updated are exposed. Suggest holding this until that release is out.

The redirect itself is verified working end to end: fresh install of the bare repo URL registers the Go node as `pr-af`; an existing Python `pr-af` is replaced in place with its node-scoped secrets intact; the built binary registers on a live control plane as `node_id: pr-af` with reasoners at `pr-af.*` and no `pr-af-go` residue.

@AbirAbbas
AbirAbbas merged commit 8f1abeb into main Aug 4, 2026
4 checks passed
@AbirAbbas
AbirAbbas deleted the feat/consolidate-go-as-pr-af branch August 4, 2026 23:41
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>
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