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

@@ -26,7 +26,7 @@ const { VALID_PROFILES, getAgentToModelMapForProfile, formatAgentToModelMapAsTab
import configSchema = require('./config-schema.cjs');
const { VALID_CONFIG_KEYS, isValidConfigKey, getCapabilityConfigSchema } = configSchema;
import { isSecretKey, maskSecret } from './secrets.cjs';
import { normalizeConfiguredDefaultReviewers } from './review-reviewer-selection.cjs';
import { normalizeConfiguredDefaultReviewers, INSTANCE_NAME_PATTERN, KNOWN_REVIEWER_SLUGS } from './review-reviewer-selection.cjs';
import { migrateOnDisk } from './configuration.cjs';
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -695,6 +695,33 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
parsedValue = normalized.values;
}
// #1517: validate review.reviewer_instances.<name>.<field> leaves at the
// invocation boundary (Postel/Kerckhoffs — strict at accept). The config
// schema dynamic pattern admits the path; this block validates the name + the
// field value so a misconfigured instance is rejected at config-set time, not
// silently at review time. Single-source validators live in
// review-reviewer-selection.cjs (INSTANCE_NAME_PATTERN, KNOWN_REVIEWER_SLUGS).
const instanceLeaf = kp.match(/^review\.reviewer_instances\.([a-zA-Z0-9_-]+)\.(cli|model|agent)$/);
if (instanceLeaf) {
const [, instanceName, field] = instanceLeaf;
if (!INSTANCE_NAME_PATTERN.test(instanceName)) {
error(`Invalid reviewer instance name '${instanceName}'. Must match ^[a-z0-9][a-z0-9-]*$.`);
}
if (KNOWN_REVIEWER_SLUGS.includes(instanceName)) {
error(`Reviewer instance name '${instanceName}' must not equal a built-in reviewer slug.`);
}
if (field === 'cli') {
if (typeof parsedValue !== 'string' || !KNOWN_REVIEWER_SLUGS.includes(parsedValue)) {
error(`Invalid reviewer_instances.${instanceName}.cli '${val}'. Must be a known reviewer adapter: ${KNOWN_REVIEWER_SLUGS.join(', ')}.`);
}
} else {
// model | agent — opaque pass-through strings (never interpolated into shell).
if (typeof parsedValue !== 'string') {
error(`Invalid reviewer_instances.${instanceName}.${field} '${val}'. Must be a string.`);
}
}
}
const setConfigValueResult = setConfigValue(cwd, kp, parsedValue);
// Mask secrets in both JSON and text output. The plaintext is written