Files
msd-core/gsd-core/references/reviewer-instances.md
Tom Boucher 40ce95f882 fix(#2358): scope review.md and ship.md temp files to a per-run mktemp directory (#2433)
* fix(#2358): scope review workflow temp files to a per-run mktemp dir

/gsd-review wrote every prompt/section/output temp file to a hardcoded
/tmp path keyed only on the phase number, so two GSD projects sharing
a small phase number collide on the exact same path and a crashed
run's leftover file becomes bait a later, unrelated run can silently
read. ship.md's external peer-review stderr capture was strictly
worse — one shared, unqualified path across every project/phase/run.

Thread a single mktemp -d "${TMPDIR:-/tmp}/gsd-review.XXXXXX" run
directory through every review.md temp path (67 sites) via a new
{run_dir}/$RUN_DIR placeholder, mirroring the existing {phase}
substitution mechanism, and clean it up at the end of the run. Route
ship.md's stderr capture through a per-run mktemp file the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): regenerate fixtures + lint gate-prep

* fix(#2358): repair failing tests after gate verification

* fix(#2358): thread RUN_DIR scoping into reviewer-instances.md (#1517)

review.md's own invoke_reviewers step lazily loads
gsd-core/references/reviewer-instances.md for the review.reviewer_instances
codepath, but that doc was missed when review.md and ship.md were moved to
the run-scoped {run_dir} temp directory. It still read the combined prompt
from the old /tmp/gsd-review-prompt-{phase}.md (which build_prompt no longer
writes, breaking reviewer-instances functionality outright) and wrote each
instance's output to the old unscoped /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md,
leaving the exact cross-project temp-file collision bug open for that code
path. Both paths now thread through {run_dir}, matching every other reviewer
block in review.md.

Extends the existing #2358 regression test with assertions pinning
reviewer-instances.md's prompt read and output write to {run_dir}, and adds
the Fixed changeset fragment. Regenerated the golden-install-parity content
hashes for reviewer-instances.md via `npm run gen:golden` (paths unchanged;
only the modified file's hash moved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): backfill changeset pr (#2433)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:03 -04:00

5.0 KiB

Reviewer Instances (#1517)

Custom reviewer instances for /gsd:review: run one model-capable adapter (e.g. OpenCode) as several independent reviewer identities in a single review pass. Loaded lazily by gsd-core/workflows/review.md when review.reviewer_instances is configured. See ADR-1517 for the contract.


Config shape

A review.reviewer_instances object under the review namespace. Each entry maps an instance name to { cli, model?, agent? }:

{
  "review": {
    "reviewer_instances": {
      "opencode-deepseek": { "cli": "opencode", "model": "deepseek/deepseek-v4-pro", "agent": "review" },
      "opencode-mimo":     { "cli": "opencode", "model": "xiaomi/mimo-v2.5-pro" }
    },
    "default_reviewers": ["opencode-deepseek", "opencode-mimo", "codex"]
  }
}
  • Instance name: ^[a-z0-9][a-z0-9-]*$, must not equal a built-in slug. Validated 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 under the run-scoped {run_dir} (RUN_DIR from gather_context, #2358 — never a bare {phase}-keyed /tmp path) and runs as a distinct reviewer identity.

For an OpenCode-backed instance (the motivating adapter):

# $INSTANCE_MODEL / $INSTANCE_AGENT come from the instance spec; $INSTANCE_NAME is the
# reviewer identity (e.g. opencode-deepseek). --agent is OpenCode's native subagent flag;
# omit it when the instance has no agent. {run_dir} is the run-scoped mktemp directory
# created once in gather_context (#2358) — same directory every other reviewer block uses.
if [ -n "$INSTANCE_AGENT" ] && [ "$INSTANCE_AGENT" != "null" ]; then
  cat {run_dir}/gsd-review-prompt.md | opencode run --model "$INSTANCE_MODEL" --agent "$INSTANCE_AGENT" - 2>/dev/null > {run_dir}/gsd-review-${INSTANCE_NAME}.md
else
  cat {run_dir}/gsd-review-prompt.md | opencode run --model "$INSTANCE_MODEL" - 2>/dev/null > {run_dir}/gsd-review-${INSTANCE_NAME}.md
fi
if [ ! -s {run_dir}/gsd-review-${INSTANCE_NAME}.md ]; then
  echo "OpenCode review ($INSTANCE_NAME) failed or returned empty output." > {run_dir}/gsd-review-${INSTANCE_NAME}.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 {run_dir}/gsd-review-${INSTANCE_NAME}.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.