From b0d5ca3379ba36d8bb3fdbc8c8df195db4092e3f Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 26 Jun 2026 23:35:04 -0400 Subject: [PATCH] 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 --- .changeset/sharp-otters-romp.md | 5 + commands/gsd/review.md | 6 + docs/CONFIGURATION.md | 43 ++++ docs/INVENTORY-MANIFEST.json | 1 + docs/INVENTORY.md | 1 + .../1517-reviewer-instances-config-surface.md | 104 ++++++++ .../bin/shared/config-schema.manifest.json | 5 + gsd-core/references/reviewer-instances.md | 99 ++++++++ gsd-core/workflows/review.md | 26 ++ src/config.cts | 29 ++- src/review-reviewer-selection.cts | 178 ++++++++++++- .../golden-install-parity/antigravity.json | 7 +- .../golden-install-parity/augment.json | 9 +- .../golden-install-parity/claude.json | 7 +- .../fixtures/golden-install-parity/cline.json | 7 +- .../golden-install-parity/codebuddy.json | 9 +- .../fixtures/golden-install-parity/codex.json | 7 +- .../golden-install-parity/copilot.json | 7 +- .../golden-install-parity/cursor.json | 9 +- .../golden-install-parity/gemini.json | 7 +- .../golden-install-parity/hermes.json | 7 +- .../fixtures/golden-install-parity/kilo.json | 9 +- .../fixtures/golden-install-parity/kimi.json | 7 +- .../golden-install-parity/opencode.json | 9 +- .../fixtures/golden-install-parity/qwen.json | 7 +- .../fixtures/golden-install-parity/trae.json | 7 +- .../golden-install-parity/windsurf.json | 5 +- .../review-reviewer-instances-config.test.cjs | 48 ++++ tests/review-reviewer-instances.test.cjs | 234 ++++++++++++++++++ tests/workflow-size-baseline.json | 2 +- 30 files changed, 835 insertions(+), 66 deletions(-) create mode 100644 .changeset/sharp-otters-romp.md create mode 100644 docs/adr/1517-reviewer-instances-config-surface.md create mode 100644 gsd-core/references/reviewer-instances.md create mode 100644 tests/review-reviewer-instances-config.test.cjs create mode 100644 tests/review-reviewer-instances.test.cjs diff --git a/.changeset/sharp-otters-romp.md b/.changeset/sharp-otters-romp.md new file mode 100644 index 000000000..24193c9cc --- /dev/null +++ b/.changeset/sharp-otters-romp.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1766 +--- +**`/gsd-review` now supports custom reviewer instances** — run one model-capable adapter (e.g. OpenCode) as several independent reviewer identities via a bounded `review.reviewer_instances` config, so two different models can review in a single pass without manually swapping config or hand-merging REVIEWS.md. (#1517) diff --git a/commands/gsd/review.md b/commands/gsd/review.md index dccea17be..52096c30c 100644 --- a/commands/gsd/review.md +++ b/commands/gsd/review.md @@ -35,6 +35,12 @@ Phase number: extracted from $ARGUMENTS (required) - `--cursor` — Include Cursor agent review - `--agy` / `--antigravity` — Include Antigravity CLI review - `--all` — Include all available CLIs + +**No flags** — if `review.default_reviewers` is set, review with only those configured +reviewers that are detected; otherwise review with all available CLIs. Configured +`review.reviewer_instances` names may appear in `review.default_reviewers`; each runs as an +independent reviewer identity backed by its configured adapter+model (see +`docs/CONFIGURATION.md`). Instance names are not valid as flags. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d86fe7ccb..f2c56d050 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -224,6 +224,49 @@ Example: } ``` +### Reviewer instances for `/gsd-review` (#1517) + +Use `review.reviewer_instances` to run one model-capable adapter as several independent +reviewer identities — e.g. two OpenCode-backed reviews with different models in a single +`/gsd-review` pass. Each entry maps an instance name to `{ cli, model?, agent? }`. + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `review.reviewer_instances..cli` | string | (required) | A known reviewer adapter the instance reuses (e.g. `opencode`). Must be a built-in slug; never an arbitrary shell command. | +| `review.reviewer_instances..model` | string | (adapter default) | Opaque `provider/model` id passed through verbatim to the adapter's `--model`. GSD does not parse it. | +| `review.reviewer_instances..agent` | string | (none) | Opaque agent name; honoured only by adapters with a native agent concept (OpenCode `--agent` in v1). | + +Instance names must match `^[a-z0-9][a-z0-9-]*$` and must not equal a built-in reviewer slug. +Instances participate ONLY through `review.default_reviewers` (there are no per-instance CLI +flags). Instance references are expanded before built-in slugs; an instance is available iff +its `cli` is detected. An entry that is neither a defined instance nor a built-in slug is a +hard error (a typo'd instance name must be loud). When two or more selected instances share +the same `cli`, `REVIEWS.md` prints a one-line shared-adapter caveat so review consensus is +not silently overstated. See [ADR-1517](adr/1517-reviewer-instances-config-surface.md). + +Example: + +```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"] + } +} +``` + +Set each field via `config-set`: + +```bash +gsd config-set review.reviewer_instances.opencode-deepseek.cli opencode +gsd config-set review.reviewer_instances.opencode-deepseek.model deepseek/deepseek-v4-pro +gsd config-set review.reviewer_instances.opencode-deepseek.agent review +gsd config-set review.default_reviewers '["opencode-deepseek","opencode-mimo","codex"]' +``` + ### Agent-skill injection (dynamic) `agent_skills.` extends the `agent_skills` map documented below. Slug is validated against `[a-zA-Z0-9_-]+` — no path separators, no whitespace, no shell metacharacters. Configured interactively via `/gsd-config --integrations`. diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index d5296bf0d..113b5ad54 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -248,6 +248,7 @@ "research-documentation-lookup.md", "research-philosophy.md", "research-verification-protocol.md", + "reviewer-instances.md", "revision-loop.md", "scout-codebase.md", "security-asvs-levels.md", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index e9b6f3c87..71cdcd970 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -312,6 +312,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `loop-hook-dispatch.md` | Generic dispatch contract for consuming `gsd_run loop render-hooks --raw` output in any host-loop workflow — envelope shape, per-kind dispatch rules (contribution/step/gate), and liveness banner. | | `scout-codebase.md` | Phase-type→codebase-map selection table for discuss-phase scout step (extracted via the discuss-phase/modes progressive-disclosure split, #717). | | `revision-loop.md` | Plan revision iteration patterns. | +| `reviewer-instances.md` | Custom reviewer instances for `/gsd-review` (#1517) — same-adapter multi-model review: config shape, resolution rules, invocation, and the REVIEWS.md contract. Lazily loaded by `review.md` when `review.reviewer_instances` is configured. | | `universal-anti-patterns.md` | Universal anti-patterns to detect and avoid. | | `worktree-branch-check.md` | Canonical spawn-time worktree HEAD/base guard (worktree_branch_check): verify-only and fail-closed — per-agent-branch assertion, protected-ref refusal (#2924), and an exact-base assertion that halts with `exit 42` on mismatch so the orchestrator (worktree lifecycle owner) performs recovery (#48). Embedded into worktree sub-agent prompts at dispatch. | | `worktree-path-safety.md` | Worktree guard suite: HEAD assertion, cwd-drift sentinel (step 0a, #3097), and absolute-path guard (step 0b, #3099) — loaded into executor spawn prompts via ``. | diff --git a/docs/adr/1517-reviewer-instances-config-surface.md b/docs/adr/1517-reviewer-instances-config-surface.md new file mode 100644 index 000000000..11db3ca3c --- /dev/null +++ b/docs/adr/1517-reviewer-instances-config-surface.md @@ -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, + `## Review ()`, 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 diff --git a/gsd-core/bin/shared/config-schema.manifest.json b/gsd-core/bin/shared/config-schema.manifest.json index 1aaffc4c8..fcd88656c 100644 --- a/gsd-core/bin/shared/config-schema.manifest.json +++ b/gsd-core/bin/shared/config-schema.manifest.json @@ -172,6 +172,11 @@ "source": "^review\\.max_prompt_tokens_per_reviewer\\.[a-zA-Z0-9_-]+$", "description": "review.max_prompt_tokens_per_reviewer." }, + { + "topLevel": "review", + "source": "^review\\.reviewer_instances\\.[a-zA-Z0-9_-]+\\.(cli|model|agent)$", + "description": "review.reviewer_instances.. (#1517)" + }, { "topLevel": "model_policy", "source": "^model_policy\\.runtime_tiers\\.[a-zA-Z0-9_-]+\\.(opus|sonnet|haiku)$", diff --git a/gsd-core/references/reviewer-instances.md b/gsd-core/references/reviewer-instances.md new file mode 100644 index 000000000..2d7227cc7 --- /dev/null +++ b/gsd-core/references/reviewer-instances.md @@ -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 `--` 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.`. 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.` 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.` diff --git a/gsd-core/workflows/review.md b/gsd-core/workflows/review.md index fb5658415..329a93c08 100644 --- a/gsd-core/workflows/review.md +++ b/gsd-core/workflows/review.md @@ -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 `## Review ()` 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} diff --git a/src/config.cts b/src/config.cts index 0a411140e..ab28309bc 100644 --- a/src/config.cts +++ b/src/config.cts @@ -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.. 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 diff --git a/src/review-reviewer-selection.cts b/src/review-reviewer-selection.cts index 5847972c4..9b71e0e1c 100644 --- a/src/review-reviewer-selection.cts +++ b/src/review-reviewer-selection.cts @@ -6,6 +6,14 @@ * * Owns reviewer-selection policy projection for /gsd:review: * explicit flags > --all > review.default_reviewers > all detected. + * + * Reviewer instances (#1517): a bounded config surface + * `review.reviewer_instances. = {cli, model?, agent?}` lets one + * model-capable adapter (e.g. opencode) run as several independent reviewer + * identities. Instances participate ONLY in the config_default branch (no + * per-instance CLI flags). An instance is available iff its base `cli` is + * detected. The instance→cli mapping lives HERE (single source; see the parity + * test in tests/review-reviewer-instances.test.cjs — DEFECT.GENERATIVE-FIX). */ export const KNOWN_REVIEWER_SLUGS: ReadonlyArray = [ @@ -22,17 +30,41 @@ export const KNOWN_REVIEWER_SLUGS: ReadonlyArray = [ 'llama_cpp', ]; +/** Instance names are lowercase slugs that must not shadow a built-in slug. */ +export const INSTANCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + export interface NormalizedDefaultReviewers { absent: boolean; values: string[]; errors: string[]; } +export interface ReviewerInstance { + cli: string; + model?: string; + agent?: string; +} + +export interface NormalizedReviewerInstances { + instances: Record; + errors: string[]; +} + +export interface ResolvedReviewer { + identity: string; + kind: 'builtin' | 'instance'; + cli: string; + model?: string; + agent?: string; +} + export interface ReviewerSelectionInput { detected?: unknown[]; explicitFlags?: unknown[]; allFlag?: unknown; configuredDefaultReviewers?: unknown; + /** #1517: the `review.reviewer_instances` config object. */ + reviewerInstances?: unknown; } export interface ReviewerSelectionResult { @@ -41,6 +73,10 @@ export interface ReviewerSelectionResult { warnings: string[]; infos: string[]; errors: string[]; + /** #1517: per-identity resolution (builtin slug or expanded instance). */ + resolvedInstances: ResolvedReviewer[]; + /** #1517: true when ≥2 selected instances share a base cli (consensus caveat). */ + sharedAdapterCaveat: boolean; } export function normalizeConfiguredDefaultReviewers( @@ -86,6 +122,76 @@ export function normalizeConfiguredDefaultReviewers( return { absent: false, values: normalized, errors }; } +/** + * Validate the `review.reviewer_instances` config object (#1517). + * `cli` MUST be a known adapter (never an arbitrary shell command — Kerckhoffs / + * Postel: strict at the invocation boundary). `model`/`agent` are opaque + * pass-through strings; they are never interpolated into shell strings by this + * module. Instance names must not collide with a built-in slug. + */ +export function normalizeReviewerInstances( + rawValue: unknown, +): NormalizedReviewerInstances { + if (rawValue === undefined || rawValue === null) { + return { instances: {}, errors: [] }; + } + if (typeof rawValue !== 'object' || Array.isArray(rawValue)) { + return { + instances: {}, + errors: ['review.reviewer_instances must be a JSON object mapping instance names to {cli,model,agent}'], + }; + } + + const obj = rawValue as Record; + const instances: Record = {}; + const errors: string[] = []; + + for (const [name, spec] of Object.entries(obj)) { + if (!INSTANCE_NAME_PATTERN.test(name)) { + errors.push( + `invalid reviewer instance name '${name}': must match ^[a-z0-9][a-z0-9-]*$`, + ); + continue; + } + if (KNOWN_REVIEWER_SLUGS.includes(name)) { + errors.push( + `reviewer instance name '${name}' must not equal a built-in reviewer slug`, + ); + continue; + } + if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) { + errors.push(`reviewer_instances.${name} must be an object with at least {cli}`); + continue; + } + const s = spec as Record; + const cli = s.cli; + if (typeof cli !== 'string' || !KNOWN_REVIEWER_SLUGS.includes(cli)) { + errors.push( + `reviewer_instances.${name}.cli must be a known reviewer adapter (got: ${JSON.stringify(cli)})`, + ); + continue; + } + const instance: ReviewerInstance = { cli }; + if (s.model !== undefined && s.model !== null) { + if (typeof s.model !== 'string') { + errors.push(`reviewer_instances.${name}.model must be a string`); + continue; + } + instance.model = s.model; + } + if (s.agent !== undefined && s.agent !== null) { + if (typeof s.agent !== 'string') { + errors.push(`reviewer_instances.${name}.agent must be a string`); + continue; + } + instance.agent = s.agent; + } + instances[name] = instance; + } + + return { instances, errors }; +} + export function resolveReviewerSelection( input: ReviewerSelectionInput, ): ReviewerSelectionResult { @@ -99,10 +205,13 @@ export function resolveReviewerSelection( const normalizedDefaults = normalizeConfiguredDefaultReviewers( input.configuredDefaultReviewers, ); + const normalizedInstances = normalizeReviewerInstances(input.reviewerInstances); + const instances = normalizedInstances.instances; + const instancesConfigured = Object.keys(instances).length > 0; const warnings: string[] = []; const infos: string[] = []; - const errors: string[] = [...normalizedDefaults.errors]; + const errors: string[] = [...normalizedDefaults.errors, ...normalizedInstances.errors]; let source = 'no_config_all_detected'; let selected: string[] = []; @@ -122,19 +231,37 @@ export function resolveReviewerSelection( selected = [...detected]; } else if (!normalizedDefaults.absent) { source = 'config_default'; - const knownDefaults: string[] = []; - for (const slug of normalizedDefaults.values) { - if (!KNOWN_REVIEWER_SLUGS.includes(slug)) { - warnings.push(`unknown reviewer slug in review.default_reviewers: ${slug}`); + // #1517: expand instance references BEFORE the built-in-slug check. An + // instance name and a built-in slug are the two legal kinds of entry. + for (const entry of normalizedDefaults.values) { + if (instances[entry]) { + // Instance reference — available iff its base cli is detected. + const cli = instances[entry].cli; + if (!detected.has(cli)) { + infos.push(`configured instance ${entry} not detected (cli ${cli} missing on this host)`); + } else { + selected.push(entry); + } + } else if (KNOWN_REVIEWER_SLUGS.includes(entry)) { + if (!detected.has(entry)) { + infos.push(`configured reviewers not detected on this host: ${entry}`); + } else { + selected.push(entry); + } } else { - knownDefaults.push(slug); + // Neither a defined instance nor a built-in slug. + if (instancesConfigured) { + // Most likely a typo'd instance name — must be loud (#1517 design Q2). + errors.push( + `reviewer instance '${entry}' referenced in review.default_reviewers is not defined in review.reviewer_instances`, + ); + } else { + // Backward-compatible behaviour: unknown slug with no instances + // configured warns and is dropped. + warnings.push(`unknown reviewer slug in review.default_reviewers: ${entry}`); + } } } - const undetected = knownDefaults.filter((slug) => !detected.has(slug)); - if (undetected.length > 0) { - infos.push(`configured reviewers not detected on this host: ${undetected.join(', ')}`); - } - selected = knownDefaults.filter((slug) => detected.has(slug)); if (selected.length === 0 && errors.length === 0) { errors.push('all configured default reviewers are unavailable on this host'); } @@ -142,11 +269,38 @@ export function resolveReviewerSelection( selected = [...detected]; } + const selectedSorted = selected.sort(); + + // Single-source instance→cli resolution projected onto the selected set. + const resolvedInstances: ResolvedReviewer[] = selectedSorted.map((identity) => { + const inst = instances[identity]; + if (inst) { + return { + identity, + kind: 'instance' as const, + cli: inst.cli, + model: inst.model, + agent: inst.agent, + }; + } + return { identity, kind: 'builtin' as const, cli: identity }; + }); + + const cliCounts: Record = {}; + for (const r of resolvedInstances) { + if (r.kind === 'instance') { + cliCounts[r.cli] = (cliCounts[r.cli] ?? 0) + 1; + } + } + const sharedAdapterCaveat = Object.values(cliCounts).some((c) => c >= 2); + return { source, - selected: selected.sort(), + selected: selectedSorted, warnings, infos, errors, + resolvedInstances, + sharedAdapterCaveat, }; } diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index 2c123d712..39d203a1e 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "46deb2174be356dd", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c", @@ -115,6 +115,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "a602b41bfa081fdf", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -273,7 +274,7 @@ "gsd-core/workflows/remove-phase.md": "6b9947fba1a97f46", "gsd-core/workflows/remove-workspace.md": "95f05defffe6754e", "gsd-core/workflows/resume-project.md": "cadf390bae95fc76", - "gsd-core/workflows/review.md": "439d0088fbc29350", + "gsd-core/workflows/review.md": "4d1ea3f9785044b3", "gsd-core/workflows/scan.md": "f2754f3e3ea528ff", "gsd-core/workflows/secure-phase.md": "78705b7f09612464", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -381,7 +382,7 @@ "skills/gsd-quick/SKILL.md": "c18b11b12ba134fa", "skills/gsd-resume-work/SKILL.md": "fa2ee37470c6ae07", "skills/gsd-review-backlog/SKILL.md": "d8a150558cc9326a", - "skills/gsd-review/SKILL.md": "f9b5e25043b667ac", + "skills/gsd-review/SKILL.md": "1abcf1f6a7b73ab2", "skills/gsd-secure-phase/SKILL.md": "47a4ecd2aa680fda", "skills/gsd-settings/SKILL.md": "fda7af8331acd352", "skills/gsd-ship/SKILL.md": "4529b04a357cdacd", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index e674db07b..9602d3631 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -85,7 +85,7 @@ "commands/gsd-quick.md": "a6124a2443394092", "commands/gsd-resume-work.md": "e54b929de88b11ce", "commands/gsd-review-backlog.md": "6e8a0417fab95cd3", - "commands/gsd-review.md": "4403de207a9c2582", + "commands/gsd-review.md": "87d9b3f0705afb5e", "commands/gsd-secure-phase.md": "a2320ecca4cb160b", "commands/gsd-settings.md": "53b90624a70fd530", "commands/gsd-ship.md": "81136d903d261b33", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "d030f80ca0df4fa9", "gsd-core/workflows/remove-workspace.md": "b8817a3a5907f5bc", "gsd-core/workflows/resume-project.md": "b18b51fd15cbce95", - "gsd-core/workflows/review.md": "18ab093986814bb3", + "gsd-core/workflows/review.md": "fc23a807c6c3d6d9", "gsd-core/workflows/scan.md": "54ff1ff60041d065", "gsd-core/workflows/secure-phase.md": "1d1c66ad9ea01bd2", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -449,7 +450,7 @@ "skills/gsd-ns-review/skills/debug/SKILL.md": "4b9bde6d213185f0", "skills/gsd-ns-review/skills/eval-review/SKILL.md": "61d28536af6794c9", "skills/gsd-ns-review/skills/forensics/SKILL.md": "e68fc285c39dad5e", - "skills/gsd-ns-review/skills/review/SKILL.md": "022745ee2379823c", + "skills/gsd-ns-review/skills/review/SKILL.md": "3d1cbf005e02c948", "skills/gsd-ns-review/skills/secure-phase/SKILL.md": "ff7949b3261c09fa", "skills/gsd-ns-review/skills/ui-phase/SKILL.md": "d0e75ee06eaa0165", "skills/gsd-ns-review/skills/ui-review/SKILL.md": "bdab072171d79877", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 89efd435a..372cf19ad 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -40,7 +40,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -114,6 +114,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -272,7 +273,7 @@ "gsd-core/workflows/remove-phase.md": "f76e1c2a4dd31a09", "gsd-core/workflows/remove-workspace.md": "0e73844f0f41cbc3", "gsd-core/workflows/resume-project.md": "3dcaa7abe1800d35", - "gsd-core/workflows/review.md": "c32eac3c18b11691", + "gsd-core/workflows/review.md": "1660ff860e79f9b7", "gsd-core/workflows/scan.md": "8e1bbf2eed1752ca", "gsd-core/workflows/secure-phase.md": "b2b9100ff79d6017", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -380,7 +381,7 @@ "skills/gsd-quick/SKILL.md": "bd5e4cb79bc41611", "skills/gsd-resume-work/SKILL.md": "e23d2fb963d47d04", "skills/gsd-review-backlog/SKILL.md": "1708aab6cb919223", - "skills/gsd-review/SKILL.md": "b2fe23d7725c19f4", + "skills/gsd-review/SKILL.md": "17cf20dd9a4796ff", "skills/gsd-secure-phase/SKILL.md": "a6adf4729b606d5a", "skills/gsd-settings/SKILL.md": "6d9fbddb0b00fd46", "skills/gsd-ship/SKILL.md": "9f7929947aac3c27", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 342457e30..3f92dc7d3 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -44,7 +44,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -118,6 +118,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -276,7 +277,7 @@ "gsd-core/workflows/remove-phase.md": "61b68a4af414e3c2", "gsd-core/workflows/remove-workspace.md": "411bfff942216185", "gsd-core/workflows/resume-project.md": "f8f71b5a98374b85", - "gsd-core/workflows/review.md": "897a4c72a97178f8", + "gsd-core/workflows/review.md": "a74ae666e01ceebb", "gsd-core/workflows/scan.md": "db0c48c3963f9a8b", "gsd-core/workflows/secure-phase.md": "c51b86e369574195", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -361,7 +362,7 @@ "skills/gsd-ns-review/skills/debug/SKILL.md": "2017ee8a5c39357a", "skills/gsd-ns-review/skills/eval-review/SKILL.md": "c76022e94ee30cb1", "skills/gsd-ns-review/skills/forensics/SKILL.md": "bbab359eaa388d34", - "skills/gsd-ns-review/skills/review/SKILL.md": "04c7f2a8515d97a2", + "skills/gsd-ns-review/skills/review/SKILL.md": "befdc84f5a0acf03", "skills/gsd-ns-review/skills/secure-phase/SKILL.md": "ceded474164b4e2d", "skills/gsd-ns-review/skills/ui-phase/SKILL.md": "c35f175ccc747d56", "skills/gsd-ns-review/skills/ui-review/SKILL.md": "48267dc071481a89", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index 4adbda5e2..91be3f4a1 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -85,7 +85,7 @@ "commands/gsd-quick.md": "8585535a111660d1", "commands/gsd-resume-work.md": "9c4ee571a15fefae", "commands/gsd-review-backlog.md": "41ee0337839b8e9d", - "commands/gsd-review.md": "4d1b7eaef1560fb4", + "commands/gsd-review.md": "763f91f5f823fa3a", "commands/gsd-secure-phase.md": "6daf0c899069ab74", "commands/gsd-settings.md": "57edae813aae049c", "commands/gsd-ship.md": "9190bbab8119a365", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "d030f80ca0df4fa9", "gsd-core/workflows/remove-workspace.md": "b8817a3a5907f5bc", "gsd-core/workflows/resume-project.md": "b18b51fd15cbce95", - "gsd-core/workflows/review.md": "18ab093986814bb3", + "gsd-core/workflows/review.md": "fc23a807c6c3d6d9", "gsd-core/workflows/scan.md": "54ff1ff60041d065", "gsd-core/workflows/secure-phase.md": "1d1c66ad9ea01bd2", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -450,7 +451,7 @@ "skills/gsd-quick/SKILL.md": "b895cb9835f3fc0e", "skills/gsd-resume-work/SKILL.md": "f8bc8b92dffd07b7", "skills/gsd-review-backlog/SKILL.md": "c0c743e1376c2eff", - "skills/gsd-review/SKILL.md": "7f8c2adde7ec94ba", + "skills/gsd-review/SKILL.md": "e47c285346aaba06", "skills/gsd-secure-phase/SKILL.md": "bd36a3157c28df37", "skills/gsd-settings/SKILL.md": "fbe736decd04a85d", "skills/gsd-ship/SKILL.md": "8d40fc9f9436d2c3", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 6cadb8d5a..10d4f6e37 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -76,7 +76,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -150,6 +150,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "a602b41bfa081fdf", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "335a974ea98d5d83", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -308,7 +309,7 @@ "gsd-core/workflows/remove-phase.md": "75c8ca0dbd1ce404", "gsd-core/workflows/remove-workspace.md": "a9d1dcda7755b6c9", "gsd-core/workflows/resume-project.md": "94ac2cac4c562557", - "gsd-core/workflows/review.md": "8c7288479748235f", + "gsd-core/workflows/review.md": "014d2f4d7c892e58", "gsd-core/workflows/scan.md": "29f3b65f5d9de885", "gsd-core/workflows/secure-phase.md": "858b5e1152b569ed", "gsd-core/workflows/session-report.md": "dd8fa011c9394075", @@ -396,7 +397,7 @@ "skills/gsd-quick/SKILL.md": "b4f4e711ba664aa0", "skills/gsd-resume-work/SKILL.md": "04f6c2e5b579e8c4", "skills/gsd-review-backlog/SKILL.md": "469696b944c4e7b5", - "skills/gsd-review/SKILL.md": "06044af5335989dc", + "skills/gsd-review/SKILL.md": "a45ab2fdca06793b", "skills/gsd-secure-phase/SKILL.md": "e64ad269c1e6e319", "skills/gsd-settings/SKILL.md": "fcdda8dd545622ae", "skills/gsd-ship/SKILL.md": "9615cc4c8f6de060", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 089f1aef5..cdcc2c120 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -42,7 +42,7 @@ "gsd-core/bin/gsd-tools.cjs": "46deb2174be356dd", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf", @@ -116,6 +116,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "a602b41bfa081fdf", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -274,7 +275,7 @@ "gsd-core/workflows/remove-phase.md": "e94ddfe4eabc0e08", "gsd-core/workflows/remove-workspace.md": "f28be7f32249f0d4", "gsd-core/workflows/resume-project.md": "4ebcb4acd3c29302", - "gsd-core/workflows/review.md": "58557fe552b6ff89", + "gsd-core/workflows/review.md": "7c04ed635c7e1c56", "gsd-core/workflows/scan.md": "28a2847b8d04156e", "gsd-core/workflows/secure-phase.md": "bae509fa254fe435", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -361,7 +362,7 @@ "skills/gsd-quick/SKILL.md": "d66072399c0aa371", "skills/gsd-resume-work/SKILL.md": "e3ab06060bdd9df0", "skills/gsd-review-backlog/SKILL.md": "2af20161e555fb42", - "skills/gsd-review/SKILL.md": "980080d635e66afe", + "skills/gsd-review/SKILL.md": "948c44f78429cb97", "skills/gsd-secure-phase/SKILL.md": "8666d8933e02c74f", "skills/gsd-settings/SKILL.md": "4ef66b6dc3a2b8ff", "skills/gsd-ship/SKILL.md": "ea26e3a839afb372", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 3a3d0ec19..f4e4d47c5 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -85,7 +85,7 @@ "commands/gsd-quick.md": "0061e478a896265d", "commands/gsd-resume-work.md": "a98b447fffe07e1e", "commands/gsd-review-backlog.md": "347abbe2fdad79c9", - "commands/gsd-review.md": "473b5a21ef15d510", + "commands/gsd-review.md": "14369ea01aff093f", "commands/gsd-secure-phase.md": "1631b1ecedbee373", "commands/gsd-settings.md": "8e4a2673e7c96cad", "commands/gsd-ship.md": "de9d81fe35184ae2", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "46deb2174be356dd", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "f76e1c2a4dd31a09", "gsd-core/workflows/remove-workspace.md": "dadbb14d9033ea3f", "gsd-core/workflows/resume-project.md": "3dcaa7abe1800d35", - "gsd-core/workflows/review.md": "c12c3029d8640d31", + "gsd-core/workflows/review.md": "28d673b6267b12c2", "gsd-core/workflows/scan.md": "8e1bbf2eed1752ca", "gsd-core/workflows/secure-phase.md": "7bd4c335484fd121", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -430,7 +431,7 @@ "skills/gsd-quick/SKILL.md": "0fdc1838759e2d25", "skills/gsd-resume-work/SKILL.md": "492e402cb66b1fcf", "skills/gsd-review-backlog/SKILL.md": "cf7ff999c96d2f76", - "skills/gsd-review/SKILL.md": "8f6496868bb60e10", + "skills/gsd-review/SKILL.md": "f5ebef9d2d12b2ad", "skills/gsd-secure-phase/SKILL.md": "970f53251c7adff7", "skills/gsd-settings/SKILL.md": "c06a8c93de64314e", "skills/gsd-ship/SKILL.md": "c7eeb21a15c66189", diff --git a/tests/fixtures/golden-install-parity/gemini.json b/tests/fixtures/golden-install-parity/gemini.json index bbe084e52..001063ace 100644 --- a/tests/fixtures/golden-install-parity/gemini.json +++ b/tests/fixtures/golden-install-parity/gemini.json @@ -85,7 +85,7 @@ "commands/gsd/quick.toml": "b1f6d47488560def", "commands/gsd/resume-work.toml": "5d1e36d643573d4c", "commands/gsd/review-backlog.toml": "19461ca74224422c", - "commands/gsd/review.toml": "b8097bb984942e8e", + "commands/gsd/review.toml": "8ce97adf16aec351", "commands/gsd/secure-phase.toml": "bbf0dc4d648f11fa", "commands/gsd/settings.toml": "5369a6fbdce65ea9", "commands/gsd/ship.toml": "fa360f1f63c8adec", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "d030f80ca0df4fa9", "gsd-core/workflows/remove-workspace.md": "9f5589b4b3a0639a", "gsd-core/workflows/resume-project.md": "b18b51fd15cbce95", - "gsd-core/workflows/review.md": "18ab093986814bb3", + "gsd-core/workflows/review.md": "fc23a807c6c3d6d9", "gsd-core/workflows/scan.md": "54ff1ff60041d065", "gsd-core/workflows/secure-phase.md": "a2f6a03034444f14", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index 0971e3536..b3ea4da48 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -115,6 +115,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -273,7 +274,7 @@ "gsd-core/workflows/remove-phase.md": "f27cecd8c78008ae", "gsd-core/workflows/remove-workspace.md": "977155e18b9df623", "gsd-core/workflows/resume-project.md": "849f3d49d366ff13", - "gsd-core/workflows/review.md": "7dd0a479d2595ff0", + "gsd-core/workflows/review.md": "d6c1c729a2627b2b", "gsd-core/workflows/scan.md": "cbde2b8b2b5fa5dd", "gsd-core/workflows/secure-phase.md": "5fc8fbd5e217e48d", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -381,7 +382,7 @@ "skills/gsd/gsd-ns-review/skills/debug/SKILL.md": "014bf3421ade2813", "skills/gsd/gsd-ns-review/skills/eval-review/SKILL.md": "609ee9ace8cd424c", "skills/gsd/gsd-ns-review/skills/forensics/SKILL.md": "d31dc39e02abc929", - "skills/gsd/gsd-ns-review/skills/review/SKILL.md": "0629aeccf352b059", + "skills/gsd/gsd-ns-review/skills/review/SKILL.md": "ace83e30ef7f9060", "skills/gsd/gsd-ns-review/skills/secure-phase/SKILL.md": "f615390f01694378", "skills/gsd/gsd-ns-review/skills/ui-phase/SKILL.md": "aac4a77d61281591", "skills/gsd/gsd-ns-review/skills/ui-review/SKILL.md": "ebe37a6d521dba62", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 3982d8f71..a2eacbaba 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -85,7 +85,7 @@ "command/gsd-quick.md": "704662b2172df14d", "command/gsd-resume-work.md": "e4bb44fb2e8076c0", "command/gsd-review-backlog.md": "1e99d6a7a3806fa5", - "command/gsd-review.md": "5f6efc83706fec9b", + "command/gsd-review.md": "1e14af38d6d421b3", "command/gsd-secure-phase.md": "65eb1cd3fa6430a5", "command/gsd-settings.md": "9bf5d13f13f7d49c", "command/gsd-ship.md": "c0ae46a2cbc7a2b4", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "a602b41bfa081fdf", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "f76e1c2a4dd31a09", "gsd-core/workflows/remove-workspace.md": "7bd5d1b63ef091a8", "gsd-core/workflows/resume-project.md": "3dcaa7abe1800d35", - "gsd-core/workflows/review.md": "268745662d5f5df7", + "gsd-core/workflows/review.md": "50e945dade0a5a67", "gsd-core/workflows/scan.md": "8e1bbf2eed1752ca", "gsd-core/workflows/secure-phase.md": "87fdcb37a8610e58", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -451,7 +452,7 @@ "skills/gsd-quick/SKILL.md": "605e596c680cbb1c", "skills/gsd-resume-work/SKILL.md": "0c92e4a51d1b6d94", "skills/gsd-review-backlog/SKILL.md": "27a9dbfe92e5d04c", - "skills/gsd-review/SKILL.md": "7309817dc0d54cef", + "skills/gsd-review/SKILL.md": "a967b30f10269e15", "skills/gsd-secure-phase/SKILL.md": "ceded474164b4e2d", "skills/gsd-settings/SKILL.md": "4587e4bef7b8942c", "skills/gsd-ship/SKILL.md": "4ad9695934e069ee", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 7cb59948b..21e06d8a2 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -77,7 +77,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -151,6 +151,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -309,7 +310,7 @@ "gsd-core/workflows/remove-phase.md": "d030f80ca0df4fa9", "gsd-core/workflows/remove-workspace.md": "b8817a3a5907f5bc", "gsd-core/workflows/resume-project.md": "b18b51fd15cbce95", - "gsd-core/workflows/review.md": "18ab093986814bb3", + "gsd-core/workflows/review.md": "fc23a807c6c3d6d9", "gsd-core/workflows/scan.md": "54ff1ff60041d065", "gsd-core/workflows/secure-phase.md": "1d1c66ad9ea01bd2", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -395,7 +396,7 @@ "skills/gsd-quick/SKILL.md": "e6aa7d96326fc874", "skills/gsd-resume-work/SKILL.md": "6e83a416db8253e1", "skills/gsd-review-backlog/SKILL.md": "ffc6216cfb6c3002", - "skills/gsd-review/SKILL.md": "c248b415238b49ee", + "skills/gsd-review/SKILL.md": "9cb8c2bc78c9bc51", "skills/gsd-secure-phase/SKILL.md": "9aa4378c6371e5e4", "skills/gsd-settings/SKILL.md": "dee7f5011c9a46f4", "skills/gsd-ship/SKILL.md": "b2e10966b6781b95", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 7727d0235..e09fa4f65 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -85,7 +85,7 @@ "command/gsd-quick.md": "07c02ab7547aec8e", "command/gsd-resume-work.md": "3ac18b2a8cc41066", "command/gsd-review-backlog.md": "1e99d6a7a3806fa5", - "command/gsd-review.md": "e1aee41cc2d736b5", + "command/gsd-review.md": "97bf8bc6ffff9475", "command/gsd-secure-phase.md": "b536ad6e68af3a26", "command/gsd-settings.md": "a3d2cb48b06c9b90", "command/gsd-ship.md": "062d0bb5656fa31b", @@ -110,7 +110,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -184,6 +184,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "a602b41bfa081fdf", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -342,7 +343,7 @@ "gsd-core/workflows/remove-phase.md": "863c58e99e9d630d", "gsd-core/workflows/remove-workspace.md": "4ed03c52e5c7bd4d", "gsd-core/workflows/resume-project.md": "15153ce5a4c38674", - "gsd-core/workflows/review.md": "dd336f71a0b8f228", + "gsd-core/workflows/review.md": "544103be3247bbd1", "gsd-core/workflows/scan.md": "4634caefa32a7307", "gsd-core/workflows/secure-phase.md": "9616682fe23cf042", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -451,7 +452,7 @@ "skills/gsd-quick/SKILL.md": "a9a971a0d471b64f", "skills/gsd-resume-work/SKILL.md": "aa6a7d9e88fe26b6", "skills/gsd-review-backlog/SKILL.md": "27a9dbfe92e5d04c", - "skills/gsd-review/SKILL.md": "9fe549f07cf4da07", + "skills/gsd-review/SKILL.md": "4d1897f8d2c65bdf", "skills/gsd-secure-phase/SKILL.md": "b37a0dbae84dd74a", "skills/gsd-settings/SKILL.md": "d72a537765bf690a", "skills/gsd-ship/SKILL.md": "fc88192bae429756", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 19e6d7dca..97c73a585 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "b7968e3e3af00249", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -115,6 +115,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -273,7 +274,7 @@ "gsd-core/workflows/remove-phase.md": "17c46fdcef27df2d", "gsd-core/workflows/remove-workspace.md": "1d34788a9b2272d2", "gsd-core/workflows/resume-project.md": "bba3250afbea8f09", - "gsd-core/workflows/review.md": "3d7c8df929053b79", + "gsd-core/workflows/review.md": "926f2f15676a21cc", "gsd-core/workflows/scan.md": "514d3eba81565193", "gsd-core/workflows/secure-phase.md": "a7e8edb0e5f48256", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -380,7 +381,7 @@ "skills/gsd-ns-review/skills/debug/SKILL.md": "42de44884d97d86d", "skills/gsd-ns-review/skills/eval-review/SKILL.md": "b6b6059061388363", "skills/gsd-ns-review/skills/forensics/SKILL.md": "4ef11f4cf902186f", - "skills/gsd-ns-review/skills/review/SKILL.md": "a17c6ffbcde6071a", + "skills/gsd-ns-review/skills/review/SKILL.md": "3202c2f3f3ba6759", "skills/gsd-ns-review/skills/secure-phase/SKILL.md": "a6adf4729b606d5a", "skills/gsd-ns-review/skills/ui-phase/SKILL.md": "7c9404102a9b9d74", "skills/gsd-ns-review/skills/ui-review/SKILL.md": "ef8f643abff1486b", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 64550c5af..660f180b7 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "46deb2174be356dd", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -115,6 +115,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -273,7 +274,7 @@ "gsd-core/workflows/remove-phase.md": "5a2521695edd486b", "gsd-core/workflows/remove-workspace.md": "ed8e5e30c33f1bfd", "gsd-core/workflows/resume-project.md": "ee9dcc4e3dd3e32c", - "gsd-core/workflows/review.md": "402fcc0b4b6a2794", + "gsd-core/workflows/review.md": "6529c2ee9b33f1fc", "gsd-core/workflows/scan.md": "afc48f3339293b30", "gsd-core/workflows/secure-phase.md": "882886f04d3b7e82", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", @@ -358,7 +359,7 @@ "skills/gsd-ns-review/skills/debug/SKILL.md": "9d814fba870c538e", "skills/gsd-ns-review/skills/eval-review/SKILL.md": "d42b504c0ca2dc69", "skills/gsd-ns-review/skills/forensics/SKILL.md": "250f476c4c547a0f", - "skills/gsd-ns-review/skills/review/SKILL.md": "ace2ad1db5c625c6", + "skills/gsd-ns-review/skills/review/SKILL.md": "c89722cb096066d4", "skills/gsd-ns-review/skills/secure-phase/SKILL.md": "b35ac9da51635368", "skills/gsd-ns-review/skills/ui-phase/SKILL.md": "89a29a2fa62b177e", "skills/gsd-ns-review/skills/ui-review/SKILL.md": "3e23efe03dafd691", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 6e9aa681e..7480efea7 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -41,7 +41,7 @@ "gsd-core/bin/gsd-tools.cjs": "46deb2174be356dd", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", - "gsd-core/bin/shared/config-schema.manifest.json": "67e4addbfd248a7c", + "gsd-core/bin/shared/config-schema.manifest.json": "65dea848d50969a2", "gsd-core/bin/shared/model-catalog.json": "dbe26e683236d8c6", "gsd-core/bin/shared/runtime-aliases.manifest.json": "f6c8b3af10dcfdc4", "gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904", @@ -115,6 +115,7 @@ "gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71", "gsd-core/references/research-philosophy.md": "62930e66cc979c1a", "gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679", + "gsd-core/references/reviewer-instances.md": "1412472f858b8f41", "gsd-core/references/revision-loop.md": "e55ff32dd98c63df", "gsd-core/references/scout-codebase.md": "ba266ecc18fbf172", "gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8", @@ -273,7 +274,7 @@ "gsd-core/workflows/remove-phase.md": "24559d23e008c9c3", "gsd-core/workflows/remove-workspace.md": "e958c1e932aef492", "gsd-core/workflows/resume-project.md": "c5731b8aabed70f9", - "gsd-core/workflows/review.md": "77124fdbb3e3bc7e", + "gsd-core/workflows/review.md": "21152d6ce0c4efa8", "gsd-core/workflows/scan.md": "8aa95448b1ba4a4a", "gsd-core/workflows/secure-phase.md": "550152cd82c25c00", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", diff --git a/tests/review-reviewer-instances-config.test.cjs b/tests/review-reviewer-instances-config.test.cjs new file mode 100644 index 000000000..878e3777c --- /dev/null +++ b/tests/review-reviewer-instances-config.test.cjs @@ -0,0 +1,48 @@ +'use strict'; + +/** + * Config whitelist gate for review.reviewer_instances (#1517). + * + * Behavioral test (not source-grep): proves `config-set` accepts the + * review.reviewer_instances..{cli,model,agent} paths. Without the + * dynamicKeyPatterns entry in config-schema.manifest.json, config-set rejects + * the key and this test fails — that is the regression-must-fail-first signal. + */ +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const fs = require('node:fs'); +const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs'); + +describe('config-set review.reviewer_instances (#1517)', () => { + test('accepts a known-adapter cli for an instance', (t) => { + const tmpDir = createTempProject('reviewer-instances-cli'); + t.after(() => cleanup(tmpDir)); + + const result = runGsdTools('config-set review.reviewer_instances.opencode-deepseek.cli opencode', tmpDir); + assert.ok(result.success, `config-set should accept the instance cli path: ${result.error || JSON.stringify(result)}`); + + const configPath = path.join(tmpDir, '.planning', 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual( + config.review?.reviewer_instances?.['opencode-deepseek']?.cli, + 'opencode', + 'instance cli must persist', + ); + }); + + test('accepts model and agent fields for an instance', (t) => { + const tmpDir = createTempProject('reviewer-instances-model'); + t.after(() => cleanup(tmpDir)); + + const modelRes = runGsdTools('config-set review.reviewer_instances.opencode-deepseek.model deepseek/deepseek-v4-pro', tmpDir); + assert.ok(modelRes.success, `model set failed: ${modelRes.error}`); + const agentRes = runGsdTools('config-set review.reviewer_instances.opencode-deepseek.agent review', tmpDir); + assert.ok(agentRes.success, `agent set failed: ${agentRes.error}`); + + const configPath = path.join(tmpDir, '.planning', 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual(config.review.reviewer_instances['opencode-deepseek'].model, 'deepseek/deepseek-v4-pro'); + assert.strictEqual(config.review.reviewer_instances['opencode-deepseek'].agent, 'review'); + }); +}); diff --git a/tests/review-reviewer-instances.test.cjs b/tests/review-reviewer-instances.test.cjs new file mode 100644 index 000000000..74a863a1a --- /dev/null +++ b/tests/review-reviewer-instances.test.cjs @@ -0,0 +1,234 @@ +'use strict'; + +/** + * Reviewer Instances feature (#1517) — custom reviewer instances for /gsd:review. + * + * Locks the instance-resolution contract: + * - normalizeReviewerInstances (config-set validation surface) + * - resolveReviewerSelection instance expansion (config_default branch only) + * - single-source instance→cli resolution parity (DEFECT.GENERATIVE-FIX) + * + * Regression-must-fail-first: these tests are written before the implementation + * and must demonstrate the failure (missing export / missing behaviour) before + * the resolver is extended. + */ +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); + +const { + KNOWN_REVIEWER_SLUGS, + normalizeReviewerInstances, + resolveReviewerSelection, +} = require('../gsd-core/bin/lib/review-reviewer-selection.cjs'); + +describe('normalizeReviewerInstances (#1517)', () => { + test('accepts a valid instance map and normalizes fields', () => { + const r = normalizeReviewerInstances({ + 'opencode-deepseek': { cli: 'opencode', model: 'deepseek/deepseek-v4-pro', agent: 'review' }, + 'codex-fast': { cli: 'codex', model: 'gpt-5-mini' }, + }); + assert.deepStrictEqual(r.errors, []); + assert.ok(r.instances['opencode-deepseek']); + assert.strictEqual(r.instances['opencode-deepseek'].cli, 'opencode'); + assert.strictEqual(r.instances['opencode-deepseek'].model, 'deepseek/deepseek-v4-pro'); + assert.strictEqual(r.instances['opencode-deepseek'].agent, 'review'); + assert.strictEqual(r.instances['codex-fast'].agent, undefined); + }); + + test('absent (undefined/null) yields an empty instance set with no errors', () => { + assert.deepStrictEqual(normalizeReviewerInstances(undefined), { instances: {}, errors: [] }); + assert.deepStrictEqual(normalizeReviewerInstances(null), { instances: {}, errors: [] }); + }); + + test('rejects a non-object value', () => { + const r = normalizeReviewerInstances('nope'); + assert.ok(r.errors.length > 0); + assert.deepStrictEqual(r.instances, {}); + }); + + test('rejects an instance name that does not match the slug pattern', () => { + const r = normalizeReviewerInstances({ + 'Opencode_Bad': { cli: 'opencode' }, // uppercase + underscore + 'opencode deepseek': { cli: 'opencode' }, // space + '9lead': { cli: 'opencode' }, // leading digit is allowed; sanity below + }); + // ^[a-z0-9][a-z0-9-]*$ → '9lead' is valid; the other two are not. + assert.ok(r.errors.some((e) => e.includes('Opencode_Bad')), `got: ${JSON.stringify(r.errors)}`); + assert.ok(r.errors.some((e) => e.includes('opencode deepseek')), `got: ${JSON.stringify(r.errors)}`); + assert.ok(r.instances['9lead'], 'leading-digit names are permitted by the pattern'); + }); + + test('rejects an instance name that collides with a built-in reviewer slug', () => { + const r = normalizeReviewerInstances({ + opencode: { cli: 'opencode' }, // shadows a built-in slug + }); + assert.ok( + r.errors.some((e) => /must not equal a built-in reviewer slug/i.test(e) && e.includes('opencode')), + `got: ${JSON.stringify(r.errors)}`, + ); + assert.ok(!r.instances['opencode']); + }); + + test('rejects an instance whose cli is not a known adapter', () => { + const r = normalizeReviewerInstances({ + 'evil-instance': { cli: 'rm -rf /' }, // not a known slug; must not become a shell command + 'weird-instance': { cli: 'totally-made-up' }, + }); + assert.ok(r.errors.some((e) => e.includes('evil-instance')), `got: ${JSON.stringify(r.errors)}`); + assert.ok(r.errors.some((e) => e.includes('weird-instance')), `got: ${JSON.stringify(r.errors)}`); + }); + + test('rejects an instance with a missing cli', () => { + const r = normalizeReviewerInstances({ + 'no-cli': { model: 'some/model' }, + }); + assert.ok(r.errors.some((e) => e.includes('no-cli') && /cli/i.test(e)), `got: ${JSON.stringify(r.errors)}`); + }); + + test('rejects non-string model / agent values', () => { + const r = normalizeReviewerInstances({ + 'bad-model': { cli: 'opencode', model: 42 }, + 'bad-agent': { cli: 'opencode', agent: { x: 1 } }, + }); + assert.ok(r.errors.some((e) => e.includes('bad-model')), `got: ${JSON.stringify(r.errors)}`); + assert.ok(r.errors.some((e) => e.includes('bad-agent')), `got: ${JSON.stringify(r.errors)}`); + }); +}); + +describe('resolveReviewerSelection — instance expansion (#1517)', () => { + const INSTANCES = { + 'opencode-deepseek': { cli: 'opencode', model: 'deepseek/deepseek-v4-pro', agent: 'review' }, + 'opencode-mimo': { cli: 'opencode', model: 'xiaomi/mimo-v2.5-pro' }, + }; + + test('two same-cli instances in default_reviewers resolve as independent identities and flag the shared adapter', () => { + const r = resolveReviewerSelection({ + detected: ['opencode', 'codex'], + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek', 'opencode-mimo', 'codex'], + reviewerInstances: INSTANCES, + }); + + assert.strictEqual(r.source, 'config_default'); + // selected carries all three identities (codex is a builtin slug) + assert.ok(r.selected.includes('opencode-deepseek')); + assert.ok(r.selected.includes('opencode-mimo')); + assert.ok(r.selected.includes('codex')); + // resolvedInstances carries the instance→cli mapping (single-source parity anchor) + const ds = r.resolvedInstances.find((x) => x.identity === 'opencode-deepseek'); + const mimo = r.resolvedInstances.find((x) => x.identity === 'opencode-mimo'); + assert.ok(ds, 'opencode-deepseek must be resolved'); + assert.ok(mimo, 'opencode-mimo must be resolved'); + assert.strictEqual(ds.kind, 'instance'); + assert.strictEqual(ds.cli, 'opencode'); + assert.strictEqual(ds.model, 'deepseek/deepseek-v4-pro'); + assert.strictEqual(ds.agent, 'review'); + assert.strictEqual(mimo.cli, 'opencode'); + // ≥2 selected instances share a cli → caveat flag set + assert.ok(r.sharedAdapterCaveat, 'two opencode instances must set sharedAdapterCaveat'); + assert.deepStrictEqual(r.errors, []); + }); + + test('a single same-cli instance does not trip the shared-adapter caveat', () => { + const r = resolveReviewerSelection({ + detected: ['opencode', 'codex'], + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek', 'codex'], + reviewerInstances: INSTANCES, + }); + assert.ok(!r.sharedAdapterCaveat); + }); + + test('an instance referenced in default_reviewers with no definition is a HARD error when instances are configured', () => { + const r = resolveReviewerSelection({ + detected: ['opencode'], + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek', 'typo-instance'], + reviewerInstances: INSTANCES, + }); + assert.ok( + r.errors.some((e) => e.includes('typo-instance')), + `expected a hard error naming the undefined instance, got: ${JSON.stringify(r.errors)}`, + ); + assert.ok(!r.selected.includes('typo-instance')); + }); + + test('backward compat: with no instances configured, an unknown slug still warns + drops (not a hard error)', () => { + const r = resolveReviewerSelection({ + detected: ['gemini'], + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['unknown_slug', 'codex'], + }); + assert.ok( + r.warnings.some((w) => w.includes('unknown_slug')), + `expected warn+drop for unknown slug when no instances configured, got: ${JSON.stringify(r.warnings)}`, + ); + assert.ok( + !r.errors.some((e) => e.includes('unknown_slug')), + 'unknown slug must NOT be a hard error when no instances are configured', + ); + }); + + test('an instance whose base cli is not detected is excluded with an info note', () => { + const r = resolveReviewerSelection({ + detected: ['codex'], // opencode not installed + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek', 'codex'], + reviewerInstances: INSTANCES, + }); + assert.ok(!r.selected.includes('opencode-deepseek')); + assert.ok( + r.infos.some((i) => i.includes('opencode-deepseek')), + `expected an info note for the undetected instance, got: ${JSON.stringify(r.infos)}`, + ); + assert.ok(r.selected.includes('codex')); + }); + + test('instances do NOT participate in the explicit_flags or all_flag branches', () => { + const explicit = resolveReviewerSelection({ + detected: ['opencode'], + explicitFlags: ['opencode'], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek'], + reviewerInstances: INSTANCES, + }); + assert.strictEqual(explicit.source, 'explicit_flags'); + assert.ok(!explicit.selected.includes('opencode-deepseek'), 'instances must not leak into explicit_flags'); + + const all = resolveReviewerSelection({ + detected: ['opencode'], + explicitFlags: [], + allFlag: true, + configuredDefaultReviewers: ['opencode-deepseek'], + reviewerInstances: INSTANCES, + }); + assert.strictEqual(all.source, 'all_flag'); + assert.ok(!all.selected.includes('opencode-deepseek'), 'instances must not leak into all_flag'); + }); + + test('parity (DEFECT.GENERATIVE-FIX): every resolved instance cli equals its configured cli field', () => { + const r = resolveReviewerSelection({ + detected: ['opencode', 'codex'], + explicitFlags: [], + allFlag: false, + configuredDefaultReviewers: ['opencode-deepseek', 'opencode-mimo', 'codex'], + reviewerInstances: INSTANCES, + }); + for (const res of r.resolvedInstances) { + if (res.kind === 'instance') { + const configured = INSTANCES[res.identity]; + assert.ok(configured, `resolved instance ${res.identity} has no configured definition`); + assert.strictEqual(res.cli, configured.cli, `cli mapping diverged for ${res.identity}`); + assert.strictEqual(res.model, configured.model, `model diverged for ${res.identity}`); + assert.strictEqual(res.agent, configured.agent, `agent diverged for ${res.identity}`); + } else { + assert.ok(KNOWN_REVIEWER_SLUGS.includes(res.identity), `builtin identity ${res.identity} is not a known slug`); + } + } + }); +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index a7bd5fdfd..afa736f81 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -63,7 +63,7 @@ "remove-phase.md": 8469, "remove-workspace.md": 7507, "resume-project.md": 17226, - "review.md": 39404, + "review.md": 40539, "scan.md": 7688, "secure-phase.md": 13476, "session-report.md": 4044,