* feat(#1517): support custom reviewer instances for /gsd:review Add a bounded review.reviewer_instances config surface so one model-capable adapter (e.g. opencode) can run as several independent reviewer identities in a single /gsd:review pass. Instances participate only via review.default_reviewers, expand before built-in slugs, are available iff their cli is detected, and a non-matching entry is a hard error (typo must be loud). >=2 same-cli instances emit a shared-adapter caveat in REVIEWS.md. Default path with no instances is byte-for-byte unchanged. Single-source instance->cli resolution lives in resolveReviewerSelection / normalizeReviewerInstances (parity-locked in tests/review-reviewer-instances.test.cjs). cli validated against KNOWN_REVIEWER_SLUGS only (never arbitrary shell); model/agent opaque, never shell-interpolated. Closes #1517 * chore(#1517): backfill changeset pr:1766 --------- Co-authored-by: review-bot <review-bot@gsd>
5.2 KiB
ADR-1517: Reviewer instances — bounded config surface for same-adapter multi-model review
- Status: Accepted
- Date: 2026-06-26
- Issue: #1517
- Builds on: Review Reviewer Selection Module, config-schema manifest (ADR-457 generated single source)
Context
/gsd:review exposes one reviewer identity per built-in slug (KNOWN_REVIEWER_SLUGS).
This works when reviewers are independent CLIs (codex, gemini), but breaks down when a
single model-capable CLI can route to several models. The motivating adapter is OpenCode:
a solo developer who wants two OpenCode-backed reviews with different models must manually
flip review.models.opencode, rerun, and hand-merge REVIEWS.md. That is easy to forget,
easy to overwrite, and does not participate in one review/convergence pass.
The feature (#1517, approved-feature) adds a bounded config surface so one adapter can
run as several independent reviewer identities. The maintainer's spec-of-record resolved the
three blocking design questions; this ADR pins the resulting contract (field names,
REVIEWS.md section-header format, frontmatter shape) because, once shipped, these become a
depended-on interface (Hyrum's Law).
Decision
Config shape
A new review.reviewer_instances object under the existing review top-level config
namespace. Each entry maps an instance name to { cli, model?, agent? }:
{
"review": {
"reviewer_instances": {
"opencode-deepseek": { "cli": "opencode", "model": "deepseek/deepseek-v4-pro", "agent": "review" },
"opencode-mimo": { "cli": "opencode", "model": "xiaomi/mimo-v2.5-pro" }
},
"default_reviewers": ["opencode-deepseek", "opencode-mimo", "codex"]
}
}
- Instance name:
^[a-z0-9][a-z0-9-]*$, MUST NOT equal a built-in slug. Validated atconfig-settime. cli: MUST be a known adapter fromKNOWN_REVIEWER_SLUGS— never an arbitrary shell command (Kerckhoffs / Postel: strict at the invocation boundary).model: a single opaqueprovider/modelstring (OpenCode's native format). GSD does NOT parse model IDs; pass through verbatim.agent: opaque string; honoured only by adapters with a native agent concept (OpenCode--agentin v1). Ignored by other adapters.
Resolution contract (single source)
Instance→cli resolution lives in ONE place: resolveReviewerSelection /
normalizeReviewerInstances in review-reviewer-selection.cjs. The /gsd:review workflow
applies the SAME rules. A parity test (tests/review-reviewer-instances.test.cjs) asserts the
resolved mapping never diverges from the configured cli field — the
DEFECT.GENERATIVE-FIX guard against two surfaces drifting.
Rules:
- Instances participate ONLY via
review.default_reviewers(no per-instance CLI flags). - Instance references expand BEFORE the built-in-slug check.
- An instance is available iff its base
cliis detected. - An entry that is neither a defined instance nor a built-in slug is a hard error when instances are configured (typo must be loud); legacy warn-and-drop when no instances are configured (backward compatibility).
- ≥2 selected instances sharing a base
clisetsharedAdapterCaveatand emit a one-line caveat in REVIEWS.md.
REVIEWS.md contract
- Frontmatter
reviewers:records actual identities: built-in slugs and instance names (e.g.[opencode-deepseek, opencode-mimo, codex]). - Section headers: each instance gets its own section,
## <Adapter> Review (<instance-name>), e.g.## OpenCode Review (opencode-deepseek). Same-cli instances are never collapsed. - Shared-adapter caveat: a one-line note after the frontmatter when ≥2 instances share an adapter, so consensus is never silently overstated.
Alternatives considered
- Per-instance CLI flags (
--opencode-1/--opencode-2): solves only one adapter, does not scale, clutters the flag surface. Rejected (spec-of-record, non-blocking decision). - Arbitrary shell commands as reviewers: maximally flexible but reintroduces quoting, portability, and injection risk. Rejected — bounded adapter config is safer.
- A parallel instance registry separate from the slug resolver: rejected via Gall's Law / Choose Boring Technology — generalize the existing slug-resolution pattern rather than bolting on a second mechanism.
Consequences
- Forward-compatibility: the field names (
cli,model,agent), the REVIEWS.md section-header format, and the frontmatter identity list are now a depended-on contract. Changing them requires a migration + a new ADR amendment. - Maintenance: a per-adapter "supported fields" matrix emerges (OpenCode: model+agent; others: model only). Bounded while the spec stays declarative.
- Security: the
cliallow-list is the trust boundary.model/agent/instance-name are opaque and never interpolated into shell strings by the resolver; the workflow passes them as separate argv elements.
Related
- #1517 — approved feature (spec-of-record in the triage comments)
src/review-reviewer-selection.cts—normalizeReviewerInstances,resolveReviewerSelectiongsd-core/bin/shared/config-schema.manifest.json—review.reviewer_instances.*dynamic pattern