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

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

Add a bounded review.reviewer_instances config surface so one model-capable
adapter (e.g. opencode) can run as several independent reviewer identities in a
single /gsd:review pass. Instances participate only via review.default_reviewers,
expand before built-in slugs, are available iff their cli is detected, and a
non-matching entry is a hard error (typo must be loud). >=2 same-cli instances
emit a shared-adapter caveat in REVIEWS.md. Default path with no instances is
byte-for-byte unchanged.

Single-source instance->cli resolution lives in resolveReviewerSelection /
normalizeReviewerInstances (parity-locked in
tests/review-reviewer-instances.test.cjs). cli validated against
KNOWN_REVIEWER_SLUGS only (never arbitrary shell); model/agent opaque, never
shell-interpolated.

Closes #1517

* chore(#1517): backfill changeset pr:1766

---------

Co-authored-by: review-bot <review-bot@gsd>
This commit is contained in:
Tom Boucher
2026-06-26 23:35:04 -04:00
committed by GitHub
parent ce62f2b68d
commit b0d5ca3379
30 changed files with 835 additions and 66 deletions

View File

@@ -0,0 +1,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)

View File

@@ -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.
</context>
<process>

View File

@@ -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.<name>.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.<name>.model` | string | (adapter default) | Opaque `provider/model` id passed through verbatim to the adapter's `--model`. GSD does not parse it. |
| `review.reviewer_instances.<name>.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.<agent-type>` 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`.

View File

@@ -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",

View File

@@ -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 <point> --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 `<execution_context>`. |

View File

@@ -0,0 +1,104 @@
# ADR-1517: Reviewer instances — bounded config surface for same-adapter multi-model review
- **Status:** Accepted
- **Date:** 2026-06-26
- **Issue:** #1517
- **Builds on:** Review Reviewer Selection Module, config-schema manifest (ADR-457 generated single source)
## Context
`/gsd:review` exposes one reviewer identity per built-in slug (`KNOWN_REVIEWER_SLUGS`).
This works when reviewers are independent CLIs (`codex`, `gemini`), but breaks down when a
single model-capable CLI can route to several models. The motivating adapter is **OpenCode**:
a solo developer who wants two OpenCode-backed reviews with different models must manually
flip `review.models.opencode`, rerun, and hand-merge `REVIEWS.md`. That is easy to forget,
easy to overwrite, and does not participate in one review/convergence pass.
The feature (#1517, `approved-feature`) adds a **bounded config surface** so one adapter can
run as several independent reviewer identities. The maintainer's spec-of-record resolved the
three blocking design questions; this ADR pins the resulting contract (field names,
REVIEWS.md section-header format, frontmatter shape) because, once shipped, these become a
depended-on interface (Hyrum's Law).
## Decision
### Config shape
A new `review.reviewer_instances` object under the existing `review` top-level config
namespace. Each entry maps an instance name to `{ cli, model?, agent? }`:
```json
{
"review": {
"reviewer_instances": {
"opencode-deepseek": { "cli": "opencode", "model": "deepseek/deepseek-v4-pro", "agent": "review" },
"opencode-mimo": { "cli": "opencode", "model": "xiaomi/mimo-v2.5-pro" }
},
"default_reviewers": ["opencode-deepseek", "opencode-mimo", "codex"]
}
}
```
- **Instance name:** `^[a-z0-9][a-z0-9-]*$`, MUST NOT equal a built-in slug. Validated at
`config-set` time.
- **`cli`:** MUST be a known adapter from `KNOWN_REVIEWER_SLUGS` — never an arbitrary shell
command (Kerckhoffs / Postel: strict at the invocation boundary).
- **`model`:** a single opaque `provider/model` string (OpenCode's native format). GSD does
NOT parse model IDs; pass through verbatim.
- **`agent`:** opaque string; honoured only by adapters with a native agent concept
(OpenCode `--agent` in v1). Ignored by other adapters.
### Resolution contract (single source)
Instance→cli resolution lives in ONE place: `resolveReviewerSelection` /
`normalizeReviewerInstances` in `review-reviewer-selection.cjs`. The `/gsd:review` workflow
applies the SAME rules. A parity test (`tests/review-reviewer-instances.test.cjs`) asserts the
resolved mapping never diverges from the configured `cli` field — the
`DEFECT.GENERATIVE-FIX` guard against two surfaces drifting.
Rules:
1. Instances participate ONLY via `review.default_reviewers` (no per-instance CLI flags).
2. Instance references expand BEFORE the built-in-slug check.
3. An instance is available iff its base `cli` is detected.
4. An entry that is neither a defined instance nor a built-in slug is a **hard error** when
instances are configured (typo must be loud); legacy warn-and-drop when no instances are
configured (backward compatibility).
5. ≥2 selected instances sharing a base `cli` set `sharedAdapterCaveat` and emit a one-line
caveat in REVIEWS.md.
### REVIEWS.md contract
- **Frontmatter `reviewers:`** records actual identities: built-in slugs and instance names
(e.g. `[opencode-deepseek, opencode-mimo, codex]`).
- **Section headers:** each instance gets its own section,
`## <Adapter> Review (<instance-name>)`, e.g. `## OpenCode Review (opencode-deepseek)`.
Same-cli instances are never collapsed.
- **Shared-adapter caveat:** a one-line note after the frontmatter when ≥2 instances share
an adapter, so consensus is never silently overstated.
## Alternatives considered
1. **Per-instance CLI flags (`--opencode-1`/`--opencode-2`):** solves only one adapter, does
not scale, clutters the flag surface. Rejected (spec-of-record, non-blocking decision).
2. **Arbitrary shell commands as reviewers:** maximally flexible but reintroduces quoting,
portability, and injection risk. Rejected — bounded adapter config is safer.
3. **A parallel instance registry separate from the slug resolver:** rejected via Gall's Law
/ Choose Boring Technology — generalize the existing slug-resolution pattern rather than
bolting on a second mechanism.
## Consequences
- **Forward-compatibility:** the field names (`cli`, `model`, `agent`), the REVIEWS.md
section-header format, and the frontmatter identity list are now a depended-on contract.
Changing them requires a migration + a new ADR amendment.
- **Maintenance:** a per-adapter "supported fields" matrix emerges (OpenCode: model+agent;
others: model only). Bounded while the spec stays declarative.
- **Security:** the `cli` allow-list is the trust boundary. `model`/`agent`/instance-name
are opaque and never interpolated into shell strings by the resolver; the workflow passes
them as separate argv elements.
## Related
- #1517 — approved feature (spec-of-record in the triage comments)
- `src/review-reviewer-selection.cts` — `normalizeReviewerInstances`, `resolveReviewerSelection`
- `gsd-core/bin/shared/config-schema.manifest.json` — `review.reviewer_instances.*` dynamic pattern

View File

@@ -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)$",

View 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.`

View File

@@ -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}

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

View File

@@ -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.<name> = {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<string> = [
@@ -22,17 +30,41 @@ export const KNOWN_REVIEWER_SLUGS: ReadonlyArray<string> = [
'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<string, ReviewerInstance>;
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<string, unknown>;
const instances: Record<string, ReviewerInstance> = {};
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<string, unknown>;
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<string, number> = {};
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,
};
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.<name>.{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');
});
});

View File

@@ -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`);
}
}
});
});

View File

@@ -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,