* 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:
@@ -172,6 +172,11 @@
|
||||
"source": "^review\\.max_prompt_tokens_per_reviewer\\.[a-zA-Z0-9_-]+$",
|
||||
"description": "review.max_prompt_tokens_per_reviewer.<reviewer-slug>"
|
||||
},
|
||||
{
|
||||
"topLevel": "review",
|
||||
"source": "^review\\.reviewer_instances\\.[a-zA-Z0-9_-]+\\.(cli|model|agent)$",
|
||||
"description": "review.reviewer_instances.<instance-name>.<cli|model|agent> (#1517)"
|
||||
},
|
||||
{
|
||||
"topLevel": "model_policy",
|
||||
"source": "^model_policy\\.runtime_tiers\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$",
|
||||
|
||||
99
gsd-core/references/reviewer-instances.md
Normal file
99
gsd-core/references/reviewer-instances.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 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](../docs/adr/1517-reviewer-instances-config-surface.md) for the contract.
|
||||
|
||||
---
|
||||
|
||||
## Config shape
|
||||
|
||||
A `review.reviewer_instances` object under the `review` 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 (`KNOWN_REVIEWER_SLUGS`) — never an arbitrary shell command.
|
||||
- `model`: opaque `provider/model` string, passed through verbatim. GSD does not parse it.
|
||||
- `agent`: opaque string; honoured only by adapters with a native agent concept (OpenCode
|
||||
`--agent` in 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`).
|
||||
|
||||
1. Instances participate ONLY via `review.default_reviewers`. They never appear under `--all`
|
||||
or explicit `--<cli>` flags, and there are no per-instance CLI flags.
|
||||
2. Expand instance references BEFORE the built-in-slug check: an entry that is a key in
|
||||
`review.reviewer_instances` is an **instance**; an entry that is a built-in slug is a
|
||||
**builtin**.
|
||||
3. An instance is **available** iff its base `cli` is detected (e.g. `opencode-deepseek` is
|
||||
available iff `opencode` is available).
|
||||
4. 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_instances` is absent entirely, fall back to the legacy unknown-slug
|
||||
warn-and-drop behaviour for backward compatibility.)
|
||||
5. `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):
|
||||
|
||||
```bash
|
||||
# $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.`
|
||||
@@ -66,6 +66,11 @@ Reviewer-selection precedence:
|
||||
- Known-but-undetected slugs emit an info note and are ignored
|
||||
- If all configured reviewers are unavailable, fail with an actionable message
|
||||
|
||||
**Reviewer instances (#1517, optional):** if `review.reviewer_instances` is configured,
|
||||
instance names in `review.default_reviewers` run as independent identities. Resolution rules
|
||||
are in `gsd-core/references/reviewer-instances.md` — load it lazily only when instances are
|
||||
configured. Unconfigured → default path unchanged.
|
||||
|
||||
If no CLIs are available:
|
||||
```
|
||||
No external AI CLIs found. Install at least one:
|
||||
@@ -243,6 +248,10 @@ else
|
||||
fi
|
||||
```
|
||||
|
||||
**Reviewer instances (#1517, optional):** when instances are configured, each selected
|
||||
instance invokes its base `cli` with its own `model`/`agent` (opaque argv, never
|
||||
shell-interpolated). Exact invocation in `gsd-core/references/reviewer-instances.md`.
|
||||
|
||||
For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
|
||||
|
||||
**Gemini:**
|
||||
@@ -634,6 +643,11 @@ Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`:
|
||||
|
||||
After all reviewers complete, collect trim metadata files written during the run. For each reviewer that was trimmed (i.e. a `.metadata.json` file exists and `hardFailed` or `omitted` is non-empty, or `projectMdShrunk` is true, or `planTruncationPct > 0`), include a `trimmed_reviewers` block in the frontmatter. Omit the key entirely if no reviewer was trimmed.
|
||||
|
||||
**Reviewer instances (#1517, optional):** when instances ran, frontmatter records their
|
||||
names, each gets its own `## <Adapter> Review (<instance>)` section, and ≥2 same-cli
|
||||
instances print a one-line shared-adapter caveat. Format in
|
||||
`gsd-core/references/reviewer-instances.md`.
|
||||
|
||||
```markdown
|
||||
---
|
||||
phase: {N}
|
||||
@@ -684,6 +698,18 @@ trimmed_reviewers: # only present if at least one reviewer was trimmed
|
||||
|
||||
---
|
||||
|
||||
## OpenCode Review (opencode-deepseek)
|
||||
|
||||
{opencode-deepseek instance review content — only present when this instance was selected}
|
||||
|
||||
---
|
||||
|
||||
## OpenCode Review (opencode-mimo)
|
||||
|
||||
{opencode-mimo instance review content — only present when this instance was selected}
|
||||
|
||||
---
|
||||
|
||||
## Qwen Review
|
||||
|
||||
{qwen review content}
|
||||
|
||||
Reference in New Issue
Block a user