Skip to content

fix(review): honour the configured adapter, and let review be pinned apart from the rest (AGT-4292) - #616

Merged
unohee merged 1 commit into
mainfrom
fix/review-adapter
Sep 10, 2026
Merged

fix(review): honour the configured adapter, and let review be pinned apart from the rest (AGT-4292)#616
unohee merged 1 commit into
mainfrom
fix/review-adapter

Conversation

@unohee

@unohee unohee commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

openswarm review read the config file for Linear settings and nothing else, so runReviewer fell through to the registry default — codex — whatever the operator had configured. The daemon honours adapter:; the standalone CLI did not. Someone running adapter: codex-responses with an OpenRouter key had no way to review on openrouter.

Review is also the one role worth pinning separately: it is the second opinion, so running it on the same provider as the work it checks is a correlated failure. reviewAdapter: exists so that choice does not force every other role onto another provider.

Precedence

--adapter > OPENSWARM_REVIEW_ADAPTER > config.reviewAdapter > config.adapter > built-in default

An unknown name throws rather than falling through to a lower-precedence source, and lists the valid options the way the registry's own error does — the operator asked for something specific and would otherwise silently get a different provider.

A flag or the environment variable short-circuits before config is read. That is not an optimisation: loadConfig is not a pure read — it engages enableHumanSurfaceReadOnly() and the sandbox executor wiring, and a plain openswarm review never triggered either before this existed.

Why adapterNames.ts is a leaf module

Importing the adapter registry for a string check pulls in codex.ts's module-scope promisify(execFile) and breaks eleven tests that mock node:child_process; putting the constant on core/config.ts breaks a test that mocks that module. Nothing in the source links the copy to the registry, so a test asserts they hold the same names — drift otherwise surfaces as a config that fails validation for an adapter the registry supports.

What the review found that this does NOT fix

Both filed rather than folded in, because bundling an unrelated root cause into an adapter-resolution change is the mistake that cost three review rounds on another PR today:

  • AGT-4298mcpClient.ts calls loadConfig() unguarded during tool auto-discovery, so review --json | jq is still broken for any review that uses tools. Predates this change. Every test here stubs deps.review, so no test in the suite runs that path.
  • AGT-4299review --max still resolves via opts.adapter ?? activeAdapter() and ignores reviewAdapter:, while review --debug reports the key as honoured. reviewAdapter is also documented nowhere.

This PR's claim is narrowed accordingly: adapter resolution no longer pollutes stdout, not "the --json contract is clean".

A guard test that passed for the wrong reason

The test named "--json writes the verdict to stdout and keeps prose off it" spied on process.stdout.write while vitest intercepts console.* above that spy — so the pollution this change briefly introduced never reached its capture array and the guard stayed green through it. It now captures both and asserts JSON.parse of the whole capture, which is what jq does.

Review

Layer 2 (independent subagent), two rounds. Round 1 found the stdout regression and the false-green guard; round 2 confirmed both fixed and found AGT-4298/4299 by building and running the real CLI. Layer 1 (openswarm review) is not run on my own commits per standing policy.

mutant tests killed
remove the console suppression 2
do not propagate --json 2
remove the short-circuit 2
phantom name in ADAPTER_NAMES 1

Gate

tsc --noEmit exit 0 · src/cli/ + src/core/ 1055 passed, exit 0 · oxlint 0 warnings on the changed files.

Closes AGT-4292.

🤖 Generated with Claude Code

…apart from the rest (AGT-4292)

`openswarm review` read the config file for Linear settings and nothing
else, so `runReviewer` fell through to the registry default — 'codex' —
whatever the operator had configured. The daemon honours `adapter:`; the
standalone CLI did not. Someone running `adapter: codex-responses` with an
OpenRouter key had no way to review on openrouter.

Review is also the one role worth pinning separately. It is the second
opinion, so running it on the same provider as the work it checks is a
correlated failure, and `reviewAdapter:` exists so that choice does not
force every other role onto another provider.

Precedence, most specific first: --adapter > OPENSWARM_REVIEW_ADAPTER >
config.reviewAdapter > config.adapter > built-in default. An unknown name
throws rather than falling through to a lower-precedence source, and names
the valid options the way the registry's own error does — the operator
asked for something specific and would otherwise silently get a different
provider.

A flag or the environment variable short-circuits before config is read.
That is not an optimisation: `loadConfig` is not a pure read, it engages
`enableHumanSurfaceReadOnly()` and the sandbox executor wiring, and a plain
`openswarm review` never triggered either before this existed. When config
IS consulted and the caller asked for JSON, `console` is silenced around
the call, because `loadConfig` logs where it loaded from straight to
stdout, in front of the JSON document.

`ADAPTER_NAMES` is a leaf module rather than a re-export. Importing the
adapter registry for a string check pulls in `codex.ts`'s module-scope
`promisify(execFile)` and breaks eleven tests that mock
`node:child_process`; putting it on `core/config.ts` breaks a test that
mocks that module. Nothing in the source links the copy to the registry, so
a test asserts they hold the same names — drift otherwise surfaces as a
config that fails validation for an adapter the registry supports.

Two things the review of this change found that it does NOT fix, both
filed rather than folded in, because bundling an unrelated root cause into
an adapter-resolution change is the mistake that cost three review rounds
elsewhere today:

  AGT-4298 — `mcpClient.ts` calls `loadConfig()` unguarded during tool
  auto-discovery, so `review --json | jq` is still broken for any review
  that uses tools. Predates this change. Every test here stubs
  `deps.review`, so no test in the suite runs that path.

  AGT-4299 — `review --max` still resolves via `opts.adapter ??
  activeAdapter()` and ignores `reviewAdapter:`, while `review --debug`
  reports the key as honoured. `reviewAdapter` is also documented nowhere.

The stdout guard test was rewritten because it passed for the wrong
reason: it spied on `process.stdout.write` while vitest intercepts
`console.*` above that spy, so the pollution never reached its capture. It
now captures both and asserts `JSON.parse` of the whole thing, which is
what `jq` does.

Review: layer 2 (independent subagent), two rounds. Round 1 found the
stdout regression and the false-green guard; round 2 confirmed those fixed
and found AGT-4298/4299 by running the real CLI. Mutants: removing the
console suppression kills 2 tests, not propagating --json 2, removing the
short-circuit 2, a phantom name in ADAPTER_NAMES 1.

tsc --noEmit exit 0 · src/cli/ + src/core/ 1055 passed, exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@unohee
unohee merged commit 17c585e into main Sep 10, 2026
7 checks passed
@unohee
unohee deleted the fix/review-adapter branch September 10, 2026 14:13
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