feat(#1517): support custom reviewer instances for /gsd:review (#1766)

* 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>
This commit is contained in:
Tom Boucher
2026-06-26 23:35:04 -04:00
committed by GitHub
parent ce62f2b68d
commit b0d5ca3379
30 changed files with 835 additions and 66 deletions

View File

@@ -0,0 +1,104 @@
# 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? }`:
```json
{
"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 at
`config-set` time.
- **`cli`:** MUST be a known adapter from `KNOWN_REVIEWER_SLUGS` — never an arbitrary shell
command (Kerckhoffs / Postel: strict at the invocation boundary).
- **`model`:** a single opaque `provider/model` string (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 `--agent` in 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:
1. Instances participate ONLY via `review.default_reviewers` (no per-instance CLI flags).
2. Instance references expand BEFORE the built-in-slug check.
3. An instance is available iff its base `cli` is detected.
4. 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).
5. ≥2 selected instances sharing a base `cli` set `sharedAdapterCaveat` and 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
1. **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).
2. **Arbitrary shell commands as reviewers:** maximally flexible but reintroduces quoting,
portability, and injection risk. Rejected — bounded adapter config is safer.
3. **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 `cli` allow-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`, `resolveReviewerSelection`
- `gsd-core/bin/shared/config-schema.manifest.json` — `review.reviewer_instances.*` dynamic pattern