* 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>
4.8 KiB
Reviewer Instances (#1517)
Custom reviewer instances for /gsd:review: run one model-capable adapter (e.g. OpenCode)
as several independent reviewer identities in a single review pass. Loaded lazily by
gsd-core/workflows/review.md when review.reviewer_instances is configured. See
ADR-1517 for the contract.
Config shape
A review.reviewer_instances object under the review 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 (KNOWN_REVIEWER_SLUGS) — never an arbitrary shell command.model: opaqueprovider/modelstring, passed through verbatim. GSD does not parse it.agent: opaque string; honoured only by adapters with a native agent concept (OpenCode--agentin v1). Ignored by other adapters.
Resolution rules (single source)
The canonical logic lives in resolveReviewerSelection / normalizeReviewerInstances in
review-reviewer-selection.cjs. Apply the SAME rules in the workflow so the two surfaces
cannot diverge (DEFECT.GENERATIVE-FIX; parity-locked in
tests/review-reviewer-instances.test.cjs).
- Instances participate ONLY via
review.default_reviewers. They never appear under--allor explicit--<cli>flags, and there are no per-instance CLI flags. - Expand instance references BEFORE the built-in-slug check: an entry that is a key in
review.reviewer_instancesis an instance; an entry that is a built-in slug is a builtin. - An instance is available iff its base
cliis detected (e.g.opencode-deepseekis available iffopencodeis available). - An entry that is NEITHER a defined instance NOR a built-in slug is a hard error (likely
a typo'd instance name) — stop and report it. Do NOT silently drop it. (When
review.reviewer_instancesis absent entirely, fall back to the legacy unknown-slug warn-and-drop behaviour for backward compatibility.) model/agent/instance-name are opaque: pass them as separate argv elements. They are NEVER interpolated into shell strings.
Invocation
For each selected INSTANCE, invoke its base cli using the instance's own model/agent —
NOT the global review.models.<cli>. Each instance writes to its OWN per-instance output file
and runs as a distinct reviewer identity.
For an OpenCode-backed instance (the motivating adapter):
# $INSTANCE_MODEL / $INSTANCE_AGENT come from the instance spec; $INSTANCE_NAME is the
# reviewer identity (e.g. opencode-deepseek). --agent is OpenCode's native subagent flag;
# omit it when the instance has no agent.
if [ -n "$INSTANCE_AGENT" ] && [ "$INSTANCE_AGENT" != "null" ]; then
cat /tmp/gsd-review-prompt-{phase}.md | opencode run --model "$INSTANCE_MODEL" --agent "$INSTANCE_AGENT" - 2>/dev/null > /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md
else
cat /tmp/gsd-review-prompt-{phase}.md | opencode run --model "$INSTANCE_MODEL" - 2>/dev/null > /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md
fi
if [ ! -s /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md ]; then
echo "OpenCode review ($INSTANCE_NAME) failed or returned empty output." > /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md
fi
For an instance backed by a DIFFERENT cli, reuse that cli's invocation block with two
substitutions: use the instance's model in place of the global review.models.<cli> value,
and write to /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md. Only opencode honours an
agent field in v1; ignore agent for other adapters.
REVIEWS.md contract
- Frontmatter
reviewers:records the actual identities invoked. For a built-in slug use the slug (opencode); for an instance use the instance name (opencode-deepseek), so frontmatter distinguishes the independent voices. Example:reviewers: [opencode-deepseek, opencode-mimo, codex]. - Section headers: each instance gets its OWN top-level section, headed with the base
adapter's display name plus the instance name in parentheses:
## OpenCode Review (opencode-deepseek). Same-cli instances are never collapsed. - Shared-adapter caveat: when ≥2 invoked instances share the same base
cli, print a one-line caveat immediately after the frontmatter (before the first section), e.g.:> Note: opencode-deepseek and opencode-mimo share the opencode adapter; their consensus is cross-model, not cross-tool.