fix(#2315): respect review.default_reviewers in bare convergence invocation (#2451)

* test(#2315): regression test for review.default_reviewers precedence

A bare /gsd-plan-review-convergence invocation (no reviewer flags) is supposed
to let users configure a persistent reviewer lineup via review.default_reviewers
and just run the loop. Instead, the orchestrator's argument parser silently
discards that configuration and forces --codex on every no-flag invocation —
with no warning that the configured reviewers were ignored.

This commit adds a regression test that fails against the pre-fix workflow
(the buggy unconditional --codex fallback is still present at this commit)
and passes after the fix lands:

- Structural: the workflow must NOT contain an unconditional
  'if [ -z "$REVIEWER_FLAGS" ]; then REVIEWER_FLAGS="--codex"; fi' line
  before the workflow.plan_review_convergence config gate.
- Structural: the workflow must query review.default_reviewers AFTER the config
  gate and document that empty REVIEWER_FLAGS lets gsd-review apply the default.
- Behavioral: matrix across {configured, unset, empty-array, explicit-flag}
  invoking the actual deployed parse + resolution blocks with a stubbed gsd_run.

Also updates two existing tests whose assertions the fix makes stale:
- #2293 behavioral: endMarker was the buggy unconditional fallback line; the
  bare invocation assertion was 'run("5") === "--codex"'. Both flip post-fix
  (endMarker is now the last --all grep line; bare invocation returns empty
  from the parse block, default applied later in step 1.5).
- command-default-claim: the pre-fix command documented '--codex (default if
  no reviewer specified)' which was the user-facing mirror of the bug. The
  assertion now requires the command to document the review.default_reviewers
  precedence.

* fix(#2315): respect review.default_reviewers in bare convergence invocation

Root cause: plan-review-convergence.md step 1 (Parse and Normalize Arguments)
contained an unconditional fallback that set REVIEWER_FLAGS=\"--codex\" whenever
no explicit reviewer flag was supplied. This value was then interpolated
verbatim into the gsd-review args, so gsd-review saw --codex as an explicit
flag (precedence rule 1) and never reached rule 3 (review.default_reviewers).
The same path silently dropped any configured review.reviewer_instances
(instances participate ONLY via review.default_reviewers per ADR-1517).

Fix:
- Remove the unconditional --codex fallback from step 1.
- Add a config-gated resolution in step 1.5 (after CONVERGENCE_ENABLED check)
  that queries review.default_reviewers and either leaves REVIEWER_FLAGS empty
  (letting gsd-review apply its own rule-3 default) or falls back to --codex
  when no default is configured — preserving the pre-fix default for
  unconfigured users (AC3).
- Replace the banner {REVIEWER_FLAGS} token with {REVIEWER_DISPLAY} so the
  startup banner reflects what will actually run (AC4), not a hardcoded value.

The fix upholds the documented precedence contract (ADR-0011, ADR-0015) that
the bug was actively violating. Explicit-flag invocations (--gemini, --all,
etc.) are unaffected (AC5).

References: #2315; ADR-0011 (review.default_reviewers precedence);
ADR-0015 (autonomous cross-AI convergence); ADR-1517 (reviewer instances).

* chore(#2315): bump plan-review-convergence.md baseline + changeset

- Bump plan-review-convergence.md size baseline 23713 → 25536 (the new
  step-1.5 default-resolution block).
- Add .changeset/plucky-yaks-roar.md documenting the user-visible change.

* fix(#2315): restore /gsd: colon syntax + skip behavioral test when jq missing

Two follow-ups to the #2315 fix discovered by gsd-test:

1. The fix commit accidentally regressed the slash-command namespace in the
   disabled-feature exit message: /gsd:plan-review-convergence (correct,
   from PR #3452) became /gsd-plan-review-convergence (retired dash syntax).
   The slash-command-namespace invariant test caught this. Restored the
   colon form.

2. The behavioral test exercises the deployed reviewer-resolution block,
   which pipes through jq. jq is a documented production dependency
   (review.md:244 "install jq if missing") and is present in every
   production deployment, but is NOT on PATH in the gsd-test linux-node{22,24}
   containers (same constraint as tests/opencode-review-reconstruction.
   property.test.cjs). Without jq, the printf|jq pipeline fails silently,
   the ||echo 0 fallback yields DEFAULT_REVIEWERS_COUNT=0, and the resolution
   falls through to the --codex branch — producing a false negative.
   Added a jqAvailable guard at module load (matching the existing pattern)
   and skip the behavioral test when jq is absent. The structural tests
   (no bash execution) still run and validate the fix.

* chore(#2315): regenerate golden-install-parity fixtures

Source changes to plan-review-convergence.md (workflow + skill mirror +
command doc) changed the install-tree hashes. Regenerated via
'npm run gen:golden' after rebuilding gsd-core/bin/lib/install-engine.cjs
('npm run build:lib') — the on-disk lib was stale relative to
src/install-engine.cts (isSymlinkedDestOptIn) and blocked fixture gen.

* test(#2315): address review findings — strengthen structural tests + property test

Code-review + security-review (isolated subagent passes) surfaced Low/Nit
findings; this commit addresses the test-side findings:

- Structural test 1 ("unconditional --codex one-liner") now asserts the
  buggy line is absent EVERYWHERE, not just before the config gate. The
  earlier assertion allowed a maintainer to re-add the line after the gate
  (passing the structural test) while the bug would still bite at runtime
  before the gate runs.
- Structural test 4 ("banner uses REVIEWER_DISPLAY") now asserts the
  LITERAL banner placeholder "Reviewers: {REVIEWER_DISPLAY}" and forbids
  "Reviewers: {REVIEWER_FLAGS}". The earlier workflow.includes(
  "REVIEWER_DISPLAY") was satisfied by a comment mention.
- New structural test for command/skill content parity: both files must
  document the review.default_reviewers precedence on the --codex flag
  (catches a manual edit to one that the gen:plugin-skills mirror misses).
- Behavioral test stub now passes default_reviewers via env var
  ($GSD_TEST_DEFAULT_REVIEWERS) instead of inline-interpolating into a
  bash single-quoted string. Removes the (currently-safe) fragility where
  a future test input containing a single quote would close the bash
  quote and execute as bash under execFileSync.
- New property test (fast-check, numRuns=25) for the JSON-classification
  contract: non-empty arrays of slugs -> empty REVIEWER_FLAGS; empty
  array / scalar JSON / malformed JSON -> --codex fallback. Locks the
  parser contract per CLAUDE.md mandate.

* fix(#2315): address review findings — defensive jq-missing warning + banner cleanup

Code-review surfaced two Low-severity workflow-side findings:

- Defensive jq-missing warning: if jq is not on PATH in production (it is
  a documented dependency per review.md:244, but the dependency can be
  absent in degraded environments), the printf|jq pipeline fails silently
  to "0" and a user with review.default_reviewers configured gets --codex
  with no indication their configured default was unreadable. Added a
  command -v jq guard at the top of the resolution that falls back to
  --codex AND emits a stderr warning explaining the reason. This makes the
  failure diagnosable instead of silently reproducing the #2315 override.

- Banner leading-space cleanup: REVIEWER_FLAGS accumulates with a leading
  space ("$REVIEWER_FLAGS --gemini" from ""), so the explicit-flag branch
  of REVIEWER_DISPLAY="$REVIEWER_FLAGS" rendered "Reviewers:  --gemini"
  (double space). Pre-existing but worth fixing alongside the AC4 banner
  work. Strip one leading space with the ${VAR# } parameter expansion in
  the explicit-flag branch only (the configured-default and --codex
  branches already produce clean strings).

* chore(#2315): bump plan-review-convergence.md baseline + regen golden fixtures

The defensive jq-missing warning grew plan-review-convergence.md
(25536 -> 26285 bytes). Bumps the per-file baseline snapshot and
regenerates the golden-install-parity fixtures for the resulting
install-tree hash changes.

* chore(#2315): backfill pr:2451 in .changeset/plucky-yaks-roar.md
This commit is contained in:
Tom Boucher
2026-07-20 10:55:28 -04:00
committed by GitHub
parent eb45fc0e8c
commit 352876ff0c
24 changed files with 362 additions and 60 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2451
---
**`/gsd-plan-review-convergence` no longer silently overrides configured reviewers with Codex** — a bare invocation (no reviewer flags) now respects `review.default_reviewers` (and, transitively, `review.reviewer_instances`) per ADR-0011/ADR-0015, instead of always injecting `--codex` and bypassing the configured default. Users without `review.default_reviewers` configured still get `--codex` as before. The startup banner now shows what will actually run.

View File

@@ -40,7 +40,7 @@ Replaces gsd-plan-phase's internal gsd-plan-checker with external AI reviewers (
Phase number: extracted from $ARGUMENTS (required)
**Flags:**
- `--codex` — Use Codex CLI as reviewer (default if no reviewer specified)
- `--codex` — Use Codex CLI as reviewer (default if no reviewer flag given AND `review.default_reviewers` is unset; otherwise `review.default_reviewers` wins per ADR-0011 — #2315)
- `--gemini` — Use Gemini CLI as reviewer
- `--agy` / `--antigravity` — Use Antigravity CLI as reviewer (successor to the discontinued Gemini CLI)
- `--claude` — Use Claude CLI as reviewer (separate session)

View File

@@ -34,7 +34,9 @@ echo "$ARGUMENTS" | grep -q '\-\-ollama' && REVIEWER_FLAGS="$REVIEWER_FLAGS --ol
echo "$ARGUMENTS" | grep -q '\-\-lm-studio' && REVIEWER_FLAGS="$REVIEWER_FLAGS --lm-studio"
echo "$ARGUMENTS" | grep -q '\-\-llama-cpp' && REVIEWER_FLAGS="$REVIEWER_FLAGS --llama-cpp"
echo "$ARGUMENTS" | grep -q '\-\-all' && REVIEWER_FLAGS="$REVIEWER_FLAGS --all"
if [ -z "$REVIEWER_FLAGS" ]; then REVIEWER_FLAGS="--codex"; fi
# #2315: do NOT default REVIEWER_FLAGS to --codex here. The default is resolved
# against review.default_reviewers in step 1.5 (after the config gate) so a bare
# invocation respects the configured reviewer lineup per ADR-0011 / ADR-0015.
MAX_CYCLES=$(echo "$ARGUMENTS" | grep -oE '\-\-max-cycles\s+[0-9]+' | awk '{print $2}')
if [ -z "$MAX_CYCLES" ]; then MAX_CYCLES=3; fi
@@ -63,6 +65,47 @@ Enable it with:
Then re-run: /gsd:plan-review-convergence {PHASE}
```
```bash
# #2315: Resolve reviewer selection when no explicit flag was given.
# The pre-fix bug unconditionally set REVIEWER_FLAGS="--codex" in step 1, BEFORE
# the config gate — silently overriding any configured review.default_reviewers
# (and, transitively, review.reviewer_instances). gsd-review sees the injected
# --codex as an explicit flag (precedence rule 1) and never reaches rule 3
# (review.default_reviewers). ADR-0011 and ADR-0015 both assume convergence
# respects review.default_reviewers on the no-flag path.
#
# After the fix: leave REVIEWER_FLAGS empty when default_reviewers is configured
# so gsd-review applies review.default_reviewers itself (rule 3). Only fall back
# to --codex when no default is configured, preserving the pre-fix default for
# unconfigured users (#2315 AC3). REVIEWER_DISPLAY mirrors the resolved value
# so the startup banner reflects what will actually run (#2315 AC4).
if [ -z "$REVIEWER_FLAGS" ]; then
DEFAULT_REVIEWERS_JSON=$(gsd_run query config-get review.default_reviewers 2>/dev/null || echo "")
if ! command -v jq >/dev/null 2>&1; then
# jq is a documented production dependency (review.md:244 — "install jq if
# missing"). If it is absent we cannot inspect the configured default, so
# fail safe with --codex and surface the reason rather than silently
# reproducing the #2315 override under degraded conditions.
echo "WARNING: jq not on PATH — cannot read review.default_reviewers; falling back to --codex (#2315)" >&2
REVIEWER_FLAGS="--codex"
REVIEWER_DISPLAY="--codex (jq missing; cannot read review.default_reviewers)"
else
DEFAULT_REVIEWERS_COUNT=$(printf '%s' "$DEFAULT_REVIEWERS_JSON" | jq 'if type=="array" then length else 0 end' 2>/dev/null || echo 0)
if [ "${DEFAULT_REVIEWERS_COUNT:-0}" -gt 0 ] 2>/dev/null; then
: # leave REVIEWER_FLAGS empty — gsd-review applies review.default_reviewers itself
REVIEWER_DISPLAY="review.default_reviewers ($(printf '%s' "$DEFAULT_REVIEWERS_JSON" | jq -r 'join(", ")' 2>/dev/null))"
else
REVIEWER_FLAGS="--codex"
REVIEWER_DISPLAY="--codex (default; configure review.default_reviewers to change)"
fi
fi
else
# Strip the leading space accumulated by the parse block so the banner renders
# "Reviewers: --gemini" not "Reviewers: --gemini" (#2315 review nit).
REVIEWER_DISPLAY="${REVIEWER_FLAGS# }"
fi
```
## 2. Initialize
```bash
@@ -91,7 +134,7 @@ Display startup banner:
GSD ► PLAN CONVERGENCE — Phase {phase_number}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Reviewers: {REVIEWER_FLAGS}
Reviewers: {REVIEWER_DISPLAY}
Max cycles: {MAX_CYCLES}
```

View File

@@ -40,7 +40,7 @@ Replaces gsd-plan-phase's internal gsd-plan-checker with external AI reviewers (
Phase number: extracted from $ARGUMENTS (required)
**Flags:**
- `--codex` — Use Codex CLI as reviewer (default if no reviewer specified)
- `--codex` — Use Codex CLI as reviewer (default if no reviewer flag given AND `review.default_reviewers` is unset; otherwise `review.default_reviewers` wins per ADR-0011 — #2315)
- `--gemini` — Use Gemini CLI as reviewer
- `--agy` / `--antigravity` — Use Antigravity CLI as reviewer (successor to the discontinued Gemini CLI)
- `--claude` — Use Claude CLI as reviewer (separate session)

View File

@@ -285,7 +285,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "ab0b22244c3389aa",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "2d71ce5e1d327b5f",
"gsd-core/workflows/plan-review-convergence.md": "b06546c4bf47e521",
"gsd-core/workflows/plant-seed.md": "6cf726be6427a35a",
"gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd",
"gsd-core/workflows/profile-user.md": "355af92ac285567f",
@@ -408,7 +408,7 @@
"skills/gsd-pause-work/SKILL.md": "9d3cc6bd70b03df1",
"skills/gsd-phase/SKILL.md": "00676bbea61410bf",
"skills/gsd-plan-phase/SKILL.md": "03a8f71d4385a50d",
"skills/gsd-plan-review-convergence/SKILL.md": "e4d52b4473c6234b",
"skills/gsd-plan-review-convergence/SKILL.md": "79d264b132ca360d",
"skills/gsd-pr-branch/SKILL.md": "5e050db73988f9a8",
"skills/gsd-profile-user/SKILL.md": "10f2ff4be2e7d55f",
"skills/gsd-progress/SKILL.md": "df0d06cf9d5963d0",

View File

@@ -80,7 +80,7 @@
"commands/gsd-pause-work.md": "4fb032f72238fe33",
"commands/gsd-phase.md": "4920d15d779329eb",
"commands/gsd-plan-phase.md": "6b05898d574f3666",
"commands/gsd-plan-review-convergence.md": "87def0355116452a",
"commands/gsd-plan-review-convergence.md": "4523045c95d4cd52",
"commands/gsd-pr-branch.md": "e168fcd545d72d0d",
"commands/gsd-profile-user.md": "ffd9c2feb4c69f11",
"commands/gsd-progress.md": "0b4d5b73a6c5e958",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
"gsd-core/workflows/plan-review-convergence.md": "8b23bf9142cf7ec7",
"gsd-core/workflows/plan-review-convergence.md": "1c31d4905c1556c8",
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "14263db831230142",
@@ -499,7 +499,7 @@
"skills/gsd-ns-workflow/skills/next/SKILL.md": "e7409245f1a0f9de",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "fe5b26417ee466be",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "0dda686ce26d9171",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "8181db9842a79620",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "32b4344ec427afce",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "68bda87136db9fb3",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "014dec52d85dcb0e",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "2f88d0fc0c1abefe",

View File

@@ -79,7 +79,7 @@
"commands/gsd-pause-work.md": "01dbaebfefacd252",
"commands/gsd-phase.md": "e8c226d2694692a5",
"commands/gsd-plan-phase.md": "7d14158eb377a248",
"commands/gsd-plan-review-convergence.md": "c24bc978d9d743dc",
"commands/gsd-plan-review-convergence.md": "cb6a8e074c0ab0cd",
"commands/gsd-pr-branch.md": "382c23a6a644c0e4",
"commands/gsd-profile-user.md": "adbc5b025b30e836",
"commands/gsd-progress.md": "75a6dc71b74c54c2",
@@ -355,7 +355,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "b810f9f2374e23a5",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "71ba7c20f15bd5f6",
"gsd-core/workflows/plan-review-convergence.md": "7db990f90908a5e4",
"gsd-core/workflows/plant-seed.md": "fbe964fcdb244802",
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",

View File

@@ -284,7 +284,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "b80df3a60c2d3f7c",
"gsd-core/workflows/plan-review-convergence.md": "35fd32d839a2e4ba",
"gsd-core/workflows/plant-seed.md": "af50e9f10d3cc6e1",
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "ff3820a27731ceb8",
@@ -406,7 +406,7 @@
"skills/gsd-pause-work/SKILL.md": "35e8a148e44f5361",
"skills/gsd-phase/SKILL.md": "00be96e7ae36c6f0",
"skills/gsd-plan-phase/SKILL.md": "f64953d1af7b0f34",
"skills/gsd-plan-review-convergence/SKILL.md": "a42e226d17b69108",
"skills/gsd-plan-review-convergence/SKILL.md": "53242a6abe90c405",
"skills/gsd-pr-branch/SKILL.md": "c5e26f2c6dff1355",
"skills/gsd-profile-user/SKILL.md": "894eb2850ecd2dde",
"skills/gsd-progress/SKILL.md": "9b288501db99c7ea",

View File

@@ -288,7 +288,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "4b0a2cb0f4f28179",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "090c31e22b1508fe",
"gsd-core/workflows/plan-review-convergence.md": "08de1c65837a8146",
"gsd-core/workflows/plan-review-convergence.md": "cb03bd703057f395",
"gsd-core/workflows/plant-seed.md": "a2cdd513663226f1",
"gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91",
"gsd-core/workflows/profile-user.md": "26f74db0a7fcd268",
@@ -403,7 +403,7 @@
"skills/gsd-ns-workflow/skills/next/SKILL.md": "3856471d0f64bf09",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "4e1363db6013a1e5",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "5796e8744314dcf5",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "d44c5d4d78cbee13",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "a7be67e1bd14b5fb",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "493f467c22d55b6b",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "605e596c680cbb1c",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "19726b83ca2c3d2d",

View File

@@ -80,7 +80,7 @@
"commands/gsd-pause-work.md": "6caa75a7c2b4dd2d",
"commands/gsd-phase.md": "e3ca4958ea20a935",
"commands/gsd-plan-phase.md": "ad988a271bf20e9c",
"commands/gsd-plan-review-convergence.md": "3d8837015534969f",
"commands/gsd-plan-review-convergence.md": "da885320edff822b",
"commands/gsd-pr-branch.md": "f5be514b9f69eaf5",
"commands/gsd-profile-user.md": "7a9289910719d828",
"commands/gsd-progress.md": "80f75f428fc0949d",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
"gsd-core/workflows/plan-review-convergence.md": "8b23bf9142cf7ec7",
"gsd-core/workflows/plan-review-convergence.md": "1c31d4905c1556c8",
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "4fa910d15dea5695",
@@ -478,7 +478,7 @@
"skills/gsd-pause-work/SKILL.md": "e2de20b7539e78d4",
"skills/gsd-phase/SKILL.md": "f87211f779315ced",
"skills/gsd-plan-phase/SKILL.md": "5f00d90224127bd5",
"skills/gsd-plan-review-convergence/SKILL.md": "bbad9a21eacdbfcf",
"skills/gsd-plan-review-convergence/SKILL.md": "1b1526f5d7a34306",
"skills/gsd-pr-branch/SKILL.md": "87bb3306488565fb",
"skills/gsd-profile-user/SKILL.md": "ee8e8547298006b8",
"skills/gsd-progress/SKILL.md": "88c8229b673004e3",

View File

@@ -44,7 +44,7 @@
".agents/skills/gsd-pause-work/SKILL.md": "b379469eed78a196",
".agents/skills/gsd-phase/SKILL.md": "25477edc97a90c91",
".agents/skills/gsd-plan-phase/SKILL.md": "0a7a22d226bcec30",
".agents/skills/gsd-plan-review-convergence/SKILL.md": "9a7ce3391c33139d",
".agents/skills/gsd-plan-review-convergence/SKILL.md": "c7d8a4db99e4f4b9",
".agents/skills/gsd-pr-branch/SKILL.md": "6901da15e321913e",
".agents/skills/gsd-profile-user/SKILL.md": "6259fabfb6afe7be",
".agents/skills/gsd-progress/SKILL.md": "85d76286162b9189",
@@ -391,7 +391,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "d838b87563feedf6",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "f10975692cbd036e",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "f5edc589cab52a7b",
"gsd-core/workflows/plan-review-convergence.md": "cfba777405d34c52",
"gsd-core/workflows/plan-review-convergence.md": "6f904fb1befa8991",
"gsd-core/workflows/plant-seed.md": "5b07de07e4593281",
"gsd-core/workflows/pr-branch.md": "d13e1cc81de40896",
"gsd-core/workflows/profile-user.md": "05828c8cc61ef384",

View File

@@ -286,7 +286,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "bb052483744f0a6d",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "a258ddcf713699ee",
"gsd-core/workflows/plan-review-convergence.md": "0afd8eb4b0689bc9",
"gsd-core/workflows/plant-seed.md": "21e461cd39e5181b",
"gsd-core/workflows/pr-branch.md": "2833905f119b5722",
"gsd-core/workflows/profile-user.md": "5cc032206c99ef71",
@@ -381,7 +381,7 @@
"skills/gsd-pause-work/SKILL.md": "4f0caa008a8001ff",
"skills/gsd-phase/SKILL.md": "d38c7f9b1d0d2360",
"skills/gsd-plan-phase/SKILL.md": "d1ba21e500bf62a8",
"skills/gsd-plan-review-convergence/SKILL.md": "d6b850077d62d723",
"skills/gsd-plan-review-convergence/SKILL.md": "29e903e25047ebd1",
"skills/gsd-pr-branch/SKILL.md": "9cd9740db385d95a",
"skills/gsd-profile-user/SKILL.md": "052a8e17ecda40f1",
"skills/gsd-progress/SKILL.md": "28b94a19f2bccb78",

View File

@@ -80,7 +80,7 @@
"commands/gsd-pause-work.md": "59630f05f95fff68",
"commands/gsd-phase.md": "9a073dcd0f934f90",
"commands/gsd-plan-phase.md": "d9bdd3726e9c3dff",
"commands/gsd-plan-review-convergence.md": "04d89318e0e23a1b",
"commands/gsd-plan-review-convergence.md": "ea3bec0ae494ecc8",
"commands/gsd-pr-branch.md": "ef2eedb0ed4295da",
"commands/gsd-profile-user.md": "0e99de36619c3b7d",
"commands/gsd-progress.md": "44a7c784dc98f735",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "3bed01c3c906ac52",
"gsd-core/workflows/plan-review-convergence.md": "86e18a63ab7a0a6f",
"gsd-core/workflows/plan-review-convergence.md": "68139431c21ef46d",
"gsd-core/workflows/plant-seed.md": "7bccd151ce7b69f2",
"gsd-core/workflows/pr-branch.md": "c67d90c65da47168",
"gsd-core/workflows/profile-user.md": "8c943983241260b5",
@@ -456,7 +456,7 @@
"skills/gsd-pause-work/SKILL.md": "7d310e7329b7ae7d",
"skills/gsd-phase/SKILL.md": "49fca93e5f2ccc70",
"skills/gsd-plan-phase/SKILL.md": "c5e1a4ab2ede399a",
"skills/gsd-plan-review-convergence/SKILL.md": "c2793cacf3f40d70",
"skills/gsd-plan-review-convergence/SKILL.md": "3e13dd1bf82f74c0",
"skills/gsd-pr-branch/SKILL.md": "a8f8838a6aa5e1dd",
"skills/gsd-profile-user/SKILL.md": "3be8d50399836ef2",
"skills/gsd-progress/SKILL.md": "1245ea9e6af6ae25",

View File

@@ -285,7 +285,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "9607e6d03e93c1c2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "62f8e4f3b475fe5f",
"gsd-core/workflows/plan-review-convergence.md": "545fa880b6c3501f",
"gsd-core/workflows/plan-review-convergence.md": "d9c282754c05eb81",
"gsd-core/workflows/plant-seed.md": "f862f77fca983749",
"gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229",
"gsd-core/workflows/profile-user.md": "de5030437226cf2c",
@@ -429,7 +429,7 @@
"skills/gsd/gsd-ns-workflow/skills/next/SKILL.md": "1d30cf6061d15168",
"skills/gsd/gsd-ns-workflow/skills/phase/SKILL.md": "610bd06d71198849",
"skills/gsd/gsd-ns-workflow/skills/plan-phase/SKILL.md": "e564bd88707a0e69",
"skills/gsd/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "87e5f3a0c1f2a6af",
"skills/gsd/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "9f67e59967b3f5eb",
"skills/gsd/gsd-ns-workflow/skills/progress/SKILL.md": "6ec0e4715dde7ee2",
"skills/gsd/gsd-ns-workflow/skills/quick/SKILL.md": "65e377345e9c49a8",
"skills/gsd/gsd-ns-workflow/skills/spec-phase/SKILL.md": "848cec32c341713c",

View File

@@ -80,7 +80,7 @@
"command/gsd-pause-work.md": "04e993b1c9f8322b",
"command/gsd-phase.md": "8f0e98dc6c223229",
"command/gsd-plan-phase.md": "8c39408407b6bf9f",
"command/gsd-plan-review-convergence.md": "45060e2c490fcae7",
"command/gsd-plan-review-convergence.md": "b957f86414b6c73f",
"command/gsd-pr-branch.md": "68e724607de3c480",
"command/gsd-profile-user.md": "9704158b2d79cad2",
"command/gsd-progress.md": "cba4805469307416",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "f10975692cbd036e",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "396dd0dbc4d7e405",
"gsd-core/workflows/plan-review-convergence.md": "232c93a9aed224fe",
"gsd-core/workflows/plant-seed.md": "ffa5774304243649",
"gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd",
"gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8",
@@ -480,7 +480,7 @@
"skills/gsd-pause-work/SKILL.md": "34366b18a392a717",
"skills/gsd-phase/SKILL.md": "64a241d4f8665aa2",
"skills/gsd-plan-phase/SKILL.md": "c95d9376440c4725",
"skills/gsd-plan-review-convergence/SKILL.md": "fb7355db56f5eaec",
"skills/gsd-plan-review-convergence/SKILL.md": "db311b1dbb0024d1",
"skills/gsd-pr-branch/SKILL.md": "a80da6aa95efc50d",
"skills/gsd-profile-user/SKILL.md": "4ac2c5ea45d17a9d",
"skills/gsd-progress/SKILL.md": "493f467c22d55b6b",

View File

@@ -349,7 +349,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
"gsd-core/workflows/plan-review-convergence.md": "8b23bf9142cf7ec7",
"gsd-core/workflows/plan-review-convergence.md": "1c31d4905c1556c8",
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "5abfae83739fa978",
@@ -443,7 +443,7 @@
"skills/gsd-pause-work/SKILL.md": "95017c70ae9dca0d",
"skills/gsd-phase/SKILL.md": "31578c329cc2583e",
"skills/gsd-plan-phase/SKILL.md": "83362394f648deeb",
"skills/gsd-plan-review-convergence/SKILL.md": "ab63cb338105d27f",
"skills/gsd-plan-review-convergence/SKILL.md": "fcac3c24e566438e",
"skills/gsd-pr-branch/SKILL.md": "67f468db29ff2cf1",
"skills/gsd-profile-user/SKILL.md": "19a1d3aba57f6c7c",
"skills/gsd-progress/SKILL.md": "cc46a92b92b989ce",

View File

@@ -80,7 +80,7 @@
"commands/gsd-pause-work.md": "bb5bf91a2e3e480e",
"commands/gsd-phase.md": "6bcda1539f949d5a",
"commands/gsd-plan-phase.md": "4e19ddc0ca22fbf6",
"commands/gsd-plan-review-convergence.md": "88c4284aa8b3dd7e",
"commands/gsd-plan-review-convergence.md": "348d02775fd62977",
"commands/gsd-pr-branch.md": "31fca4f1d6c4ee62",
"commands/gsd-profile-user.md": "725c14ae7203b5b6",
"commands/gsd-progress.md": "001f2754c427a0a9",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "3a09141de7f3dedb",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "4d51320e4e3caddc",
"gsd-core/workflows/plan-review-convergence.md": "f91c297dce1253a2",
"gsd-core/workflows/plant-seed.md": "b8dad652e31c2318",
"gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e",
"gsd-core/workflows/profile-user.md": "248d59a31948e0ed",
@@ -480,7 +480,7 @@
"skills/gsd-pause-work/SKILL.md": "c7e9ba4f242c4648",
"skills/gsd-phase/SKILL.md": "ee78c0c56c814d84",
"skills/gsd-plan-phase/SKILL.md": "4b65f0a9d7d42ec7",
"skills/gsd-plan-review-convergence/SKILL.md": "78820e21dd05d5d4",
"skills/gsd-plan-review-convergence/SKILL.md": "8338cb7e018e3713",
"skills/gsd-pr-branch/SKILL.md": "8fa5a8fa217fe913",
"skills/gsd-profile-user/SKILL.md": "3e6155a64523d59f",
"skills/gsd-progress/SKILL.md": "1dfd9327510e500c",

View File

@@ -252,7 +252,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
"gsd-core/workflows/plan-review-convergence.md": "8b23bf9142cf7ec7",
"gsd-core/workflows/plan-review-convergence.md": "1c31d4905c1556c8",
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",

View File

@@ -285,7 +285,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "c22ff5ea46de665a",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "d050d8d551ed1756",
"gsd-core/workflows/plan-review-convergence.md": "f30e8ce168471fcd",
"gsd-core/workflows/plan-review-convergence.md": "7b887180677f11ac",
"gsd-core/workflows/plant-seed.md": "0a92ba12993ac261",
"gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4",
"gsd-core/workflows/profile-user.md": "263c0693563d98da",
@@ -428,7 +428,7 @@
"skills/gsd-ns-workflow/skills/next/SKILL.md": "13e394affe675498",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "1ed640e5f06c7be6",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "ff46b5450c9c4313",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "a42e226d17b69108",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "53242a6abe90c405",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "943538c4ac6bde19",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "bd5e4cb79bc41611",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "7e4dfa2070b7d9d1",

View File

@@ -285,7 +285,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "778b73a8db6f7c32",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "619946c879f33b9d",
"gsd-core/workflows/plan-review-convergence.md": "7a527920ad606595",
"gsd-core/workflows/plan-review-convergence.md": "610d816af89b1b16",
"gsd-core/workflows/plant-seed.md": "856ad565b2eb0c47",
"gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c",
"gsd-core/workflows/profile-user.md": "672821e6b1266645",
@@ -400,7 +400,7 @@
"skills/gsd-ns-workflow/skills/next/SKILL.md": "faadd9e2817e7324",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "df3efcd61f7cc796",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "31962f06bc124c89",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "5c4475320977818f",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "b27aad9196476875",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "37a37d2cdfba46ea",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "6fd1b96274b23a9f",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "9291f835233fa8da",

View File

@@ -285,7 +285,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "e06ccd4d4c0703fb",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "80b1ba493a9a967f",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "3fed4740a91d0443",
"gsd-core/workflows/plan-review-convergence.md": "a7fde3dbf83628cc",
"gsd-core/workflows/plan-review-convergence.md": "27dcce3dbca22e3d",
"gsd-core/workflows/plant-seed.md": "6cf61f540cdbd8b9",
"gsd-core/workflows/pr-branch.md": "acd59f915d018ad4",
"gsd-core/workflows/profile-user.md": "c4313672b81b5bcd",

View File

@@ -80,7 +80,7 @@
"commands/gsd-pause-work.md": "40a953fcddbedb5d",
"commands/gsd-phase.md": "5dd3d40e3461a973",
"commands/gsd-plan-phase.md": "d6ba79bf0ede5bcd",
"commands/gsd-plan-review-convergence.md": "551597f2add981cb",
"commands/gsd-plan-review-convergence.md": "f3aab422e0708dae",
"commands/gsd-pr-branch.md": "ab1fcffe92129061",
"commands/gsd-profile-user.md": "37c9ef202669bfd3",
"commands/gsd-progress.md": "13d7e5ba8acc2605",
@@ -356,7 +356,7 @@
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
"gsd-core/workflows/plan-review-convergence.md": "8b23bf9142cf7ec7",
"gsd-core/workflows/plan-review-convergence.md": "1c31d4905c1556c8",
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
"gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b",
@@ -471,7 +471,7 @@
"skills/gsd-ns-workflow/skills/next/SKILL.md": "63659cd48a0276f9",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "b377b03d13db8573",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "4564f415845d6c15",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "12d1696eaaf60043",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "84af9aa192ec349f",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "fb24eab4a0e5f6a3",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "48629fef2b9ac5b0",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "052cfb21eb0ee3d4",

View File

@@ -31,6 +31,7 @@ const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('node:child_process');
const COMMAND_PATH = path.join(__dirname, '..', 'commands', 'gsd', 'plan-review-convergence.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-review-convergence.md');
@@ -39,6 +40,15 @@ const PLAN_PHASE_PATH = path.join(__dirname, '..', 'gsd-core', 'workflows', 'pla
const PLANNER_REVIEWS_PATH = path.join(__dirname, '..', 'gsd-core', 'references', 'planner-reviews.md');
const PLAN_CHECKER_PATH = path.join(__dirname, '..', 'agents', 'gsd-plan-checker.md');
// #2315: the workflow's reviewer-resolution block pipes through `jq`, which is
// a documented production dependency (review.md:244 "install jq if missing")
// but is NOT present in every test container (gsd-test's linux-node{22,24}
// images lack it; see tests/opencode-review-reconstruction.property.test.cjs
// for the same skip pattern). Behavioral tests that exercise the deployed jq
// pipeline skip when jq is absent; structural tests still run.
let jqAvailable = false;
try { execFileSync('jq', ['--version'], { stdio: 'ignore', timeout: 10000, killSignal: 'SIGKILL' }); jqAvailable = true; } catch { /* no jq on PATH */ }
// ─── Command source ────────────────────────────────────────────────────────
describe('plan-review-convergence command source (#2306)', () => {
@@ -123,12 +133,16 @@ describe('plan-review-convergence command source (#2306)', () => {
);
});
test('--codex is the default reviewer when no flag is specified', () => {
test('--codex is the default reviewer when no flag is given AND review.default_reviewers is unset (#2315)', () => {
// #2315: a bare invocation now respects review.default_reviewers per
// ADR-0011. The command must document that --codex is the default ONLY
// when review.default_reviewers is unset; otherwise the configured default
// wins. The pre-fix claim ("default if no reviewer specified") was the
// user-facing mirror of the #2315 bug.
assert.ok(
command.includes('default if no reviewer specified') ||
command.includes('default: --codex') ||
command.includes('(default if no reviewer specified)'),
'--codex must be documented as the default reviewer'
command.includes('default if no reviewer flag given') &&
command.includes('review.default_reviewers'),
'command must document that --codex is the default ONLY when review.default_reviewers is unset (#2315)'
);
});
@@ -159,17 +173,25 @@ describe('plan-review-convergence: --agy/--antigravity reviewer whitelist (#2293
assert.ok(skill.includes('--agy'), 'generated SKILL.md must document --agy (regenerate via gen:plugin-skills)');
});
// Behavioral: execute the ACTUAL deployed REVIEWER_FLAGS accumulation block and
// assert --antigravity passes through instead of being dropped to the --codex
// default. POSIX-only — the block is /bin/sh-style grep pipework; skip on Windows
// where a bash shim is not guaranteed on PATH.
test('[behavioral] the deployed extraction passes --antigravity through (not the --codex fallback)', (t) => {
// Behavioral: execute the ACTUAL deployed REVIEWER_FLAGS parse block and
// assert --antigravity passes through instead of being dropped. POSIX-only —
// the block is /bin/sh-style grep pipework; skip on Windows where a bash
// shim is not guaranteed on PATH.
//
// #2315: the parse block no longer applies a --codex default. The endMarker
// was previously the unconditional `if [ -z "$REVIEWER_FLAGS" ]; then
// REVIEWER_FLAGS="--codex"; fi` line; that line was the #2315 bug and is now
// gone. Default resolution moved to step 1.5 after the config gate (see the
// #2315 describe block below). The bare-invocation assertion now expects an
// empty REVIEWER_FLAGS from the parse block — the default is applied later,
// respecting review.default_reviewers.
test('[behavioral] the deployed parse block passes --antigravity through (parse block no longer applies a --codex default — #2315)', (t) => {
if (process.platform === 'win32') { t.skip('POSIX shell extraction; not run on Windows'); return; }
const { execFileSync } = require('node:child_process');
const startIdx = workflow.indexOf('REVIEWER_FLAGS=""');
const endMarker = 'if [ -z "$REVIEWER_FLAGS" ]; then REVIEWER_FLAGS="--codex"; fi';
const endMarker = "echo \"$ARGUMENTS\" | grep -q '\\-\\-all' && REVIEWER_FLAGS=\"$REVIEWER_FLAGS --all\"";
const endIdx = workflow.indexOf(endMarker);
assert.ok(startIdx !== -1 && endIdx !== -1, 'the REVIEWER_FLAGS accumulation block must exist in the workflow');
assert.ok(startIdx !== -1 && endIdx !== -1, 'the REVIEWER_FLAGS parse block must exist in the workflow');
const block = workflow.slice(startIdx, endIdx + endMarker.length) + '\nprintf "%s" "$REVIEWER_FLAGS"';
const run = (args) => execFileSync('bash', ['-c', block], {
env: { ...process.env, ARGUMENTS: args },
@@ -179,12 +201,15 @@ describe('plan-review-convergence: --agy/--antigravity reviewer whitelist (#2293
const agy = run('5 --antigravity');
assert.ok(agy.split(/\s+/).includes('--antigravity'), `--antigravity must pass through, got: "${agy}"`);
assert.notStrictEqual(agy, '--codex', 'must NOT collapse to the --codex-only fallback when --antigravity is given');
// The parse block must NOT inject --codex when an explicit flag is present.
assert.notStrictEqual(agy, '--codex', 'must NOT produce --codex-only when --antigravity is given');
assert.ok(run('5 --agy').split(/\s+/).includes('--agy'), '--agy short form must pass through');
// Regressions: default + existing flags unchanged.
assert.strictEqual(run('5'), '--codex', 'no reviewer flag → --codex default preserved');
// #2315 regression: the parse block no longer applies a default. The bare
// invocation (no flag) MUST yield an empty REVIEWER_FLAGS here; the default
// is resolved later in step 1.5 against review.default_reviewers.
assert.strictEqual(run('5'), '', 'no reviewer flag → empty REVIEWER_FLAGS from parse (default applied in step 1.5 per #2315)');
const mixed = run('5 --codex --gemini');
assert.ok(mixed.includes('--codex') && mixed.includes('--gemini'), 'existing flags still recognized');
// --agy must not be spuriously matched by an unrelated flag (independence).
@@ -192,6 +217,235 @@ describe('plan-review-convergence: --agy/--antigravity reviewer whitelist (#2293
});
});
// ─── #2315: bare invocation respects review.default_reviewers ──────────────
describe('plan-review-convergence: #2315 respects review.default_reviewers (no-flag default)', () => {
const workflow = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const command = fs.readFileSync(COMMAND_PATH, 'utf8');
const SKILL_PATH = path.join(__dirname, '..', 'skills', 'gsd-plan-review-convergence', 'SKILL.md');
const skill = fs.readFileSync(SKILL_PATH, 'utf8');
// Pre-fix #2315: the workflow unconditionally set REVIEWER_FLAGS="--codex" in
// step 1 (line 37) BEFORE the workflow.plan_review_convergence config gate.
// gsd-review sees the injected --codex as an explicit flag (precedence rule 1)
// and never reaches rule 3 (review.default_reviewers), silently overriding
// any configured default — a violation of ADR-0011 and ADR-0015.
//
// The buggy one-liner must not appear ANYWHERE in the workflow — the post-fix
// resolution lives in step 1.5 as a config-gated if/else/fi block, never as
// the bare one-liner. (Earlier versions of this test only asserted the line
// was not BEFORE the gate, which still allowed a re-introduction after the
// gate; the assertion is now unconditional per review.)
test('workflow does NOT contain the unconditional REVIEWER_FLAGS=--codex one-liner anywhere', () => {
const buggyLine = 'if [ -z "$REVIEWER_FLAGS" ]; then REVIEWER_FLAGS="--codex"; fi';
assert.strictEqual(
workflow.indexOf(buggyLine),
-1,
'the unconditional --codex one-liner must not appear anywhere in the workflow (#2315); ' +
'default resolution is conditional on review.default_reviewers in step 1.5 (if/else/fi block).'
);
});
test('workflow resolves REVIEWER_FLAGS against review.default_reviewers AFTER the config gate', () => {
const resolutionIdx = workflow.indexOf('gsd_run query config-get review.default_reviewers');
const configGateIdx = workflow.indexOf('CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence');
assert.ok(resolutionIdx !== -1, 'workflow must query review.default_reviewers to resolve the no-flag default (#2315)');
assert.ok(
resolutionIdx > configGateIdx,
'review.default_reviewers resolution must come AFTER the config gate (only runs when convergence is enabled)'
);
});
test('workflow documents that empty REVIEWER_FLAGS lets gsd-review apply review.default_reviewers', () => {
// After the fix, an empty REVIEWER_FLAGS is INTENTIONAL — it signals "let
// gsd-review apply its own precedence (rule 3: review.default_reviewers)".
// A comment must document this so a future maintainer does not re-add the
// unconditional --codex fallback and resurrect the #2315 bug.
assert.ok(
/gsd-review applies.*review\.default_reviewers|review\.default_reviewers.*gsd-review applies/i.test(workflow),
'workflow must document that empty REVIEWER_FLAGS lets gsd-review apply review.default_reviewers (prevent #2315 regression)'
);
});
test('startup banner uses REVIEWER_DISPLAY (not raw REVIEWER_FLAGS) so users see what will actually run', () => {
// AC4 of #2315: banner must reflect actual reviewers, not a hardcoded value.
// When REVIEWER_FLAGS is empty (because default_reviewers is configured),
// the banner must show the resolved default — not an empty string and not
// a misleading "--codex". Assert the literal banner placeholder so a
// maintainer cannot satisfy this by defining REVIEWER_DISPLAY in a comment
// while leaving the banner pointing at REVIEWER_FLAGS.
assert.ok(
workflow.includes('Reviewers: {REVIEWER_DISPLAY}'),
'startup banner must use the {REVIEWER_DISPLAY} placeholder, not {REVIEWER_FLAGS} (#2315 AC4)'
);
assert.ok(
!/\bReviewers:\s*\{REVIEWER_FLAGS\}/.test(workflow),
'startup banner must NOT reference {REVIEWER_FLAGS} directly (#2315 AC4)'
);
});
test('command and skill doc both document the review.default_reviewers precedence (content parity)', () => {
// The #2315 fix updated the --codex flag description in BOTH the command
// (commands/gsd/plan-review-convergence.md) and the generated skill mirror
// (skills/gsd-plan-review-convergence/SKILL.md). The two are kept in sync
// by `npm run gen:plugin-skills`; this assertion catches a manual edit to
// one that the other doesn't mirror. Both must mention review.default_reviewers
// alongside the --codex default claim.
const expected = 'review.default_reviewers';
assert.ok(
command.includes(expected),
`command file must document the review.default_reviewers precedence on the --codex flag (#2315)`
);
assert.ok(
skill.includes(expected),
`skill file must mirror the command file's review.default_reviewers precedence (#2315)`
);
});
// Behavioral: extract the parse block + the post-config-gate resolution block
// and execute them with a stubbed gsd_run. Proves the matrix:
// - bare + default_reviewers configured → empty REVIEWER_FLAGS (gsd-review applies default)
// - bare + default_reviewers unset → --codex fallback (pre-fix behavior preserved)
// - bare + empty-array default → --codex fallback (defensive — schema would reject)
// - explicit --gemini + default set → --gemini wins (explicit flags unaffected, #2315 AC5)
test('[behavioral] no-flag invocation resolves to default_reviewers when configured, --codex otherwise', (t) => {
if (process.platform === 'win32') { t.skip('POSIX shell extraction; not run on Windows'); return; }
if (!jqAvailable) { t.skip('jq not on PATH — workflow resolution block pipes through jq (production dependency, review.md:244); structural tests above still validate the fix'); return; }
const { execFileSync } = require('node:child_process');
// Parse block: from REVIEWER_FLAGS="" to the last grep line (--all).
const parseStart = workflow.indexOf('REVIEWER_FLAGS=""');
const parseEndMarker = "echo \"$ARGUMENTS\" | grep -q '\\-\\-all' && REVIEWER_FLAGS=\"$REVIEWER_FLAGS --all\"";
const parseEnd = workflow.indexOf(parseEndMarker);
assert.ok(parseStart !== -1 && parseEnd !== -1, 'parse block must exist');
const parseBlock = workflow.slice(parseStart, parseEnd + parseEndMarker.length);
// Resolution block: the `if [ -z "$REVIEWER_FLAGS" ]; then` that appears
// AFTER the config gate (CONVERGENCE_ENABLED=). This is the #2315 fix.
// Extract to the closing fence of the enclosing ```bash block — the block
// contains a nested if/else/fi, so a naive "first \nfi\n" match would stop
// at the inner fi and yield unbalanced bash.
const configGateIdx = workflow.indexOf('CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence');
const resolutionStart = workflow.indexOf('if [ -z "$REVIEWER_FLAGS" ]; then', configGateIdx);
assert.ok(resolutionStart !== -1, 'post-config-gate REVIEWER_FLAGS resolution block must exist (#2315)');
const closingFence = workflow.indexOf('\n```\n', resolutionStart);
assert.ok(closingFence !== -1, 'could not locate closing fence of resolution block');
const resolutionBlock = workflow.slice(resolutionStart, closingFence);
const run = ({ args, defaultReviewers }) => {
// Stub gsd_run: only `query config-get review.default_reviewers` is exercised.
// Empty/default → unset key (gsd_run returns nothing → empty stdout).
//
// The default_reviewers value is passed via env var ($GSD_TEST_DEFAULT_REVIEWERS)
// rather than inline-interpolated into the bash script. This avoids a quoting
// fragility: a future test input containing a single quote would otherwise
// break the bash single-quoted string and execute as bash. With env-var
// handoff, the value never crosses an interpreting shell context.
const stub = `gsd_run() { case "$*" in *"config-get review.default_reviewers"*) printf '%s' "$GSD_TEST_DEFAULT_REVIEWERS";; *) return 0;; esac; }`;
const script = `${stub}\n${parseBlock}\n${resolutionBlock}\nprintf 'REVIEWER_FLAGS=[%s] REVIEWER_DISPLAY=[%s]' "$REVIEWER_FLAGS" "$REVIEWER_DISPLAY"`;
return execFileSync('bash', ['-c', script], {
env: { ...process.env, ARGUMENTS: args, GSD_TEST_DEFAULT_REVIEWERS: defaultReviewers ?? '' },
encoding: 'utf8',
timeout: 5000,
});
};
// AC1: bare invocation with default_reviewers configured → empty REVIEWER_FLAGS
// (gsd-review applies configured default per its rule 3) and banner shows the resolved default.
let r = run({ args: '5', defaultReviewers: '["gemini","claude"]' });
assert.ok(/REVIEWER_FLAGS=\[\s*\]/.test(r), `configured default → REVIEWER_FLAGS empty, got: "${r}"`);
assert.ok(/review\.default_reviewers \(gemini, claude\)/.test(r), `banner shows configured default, got: "${r}"`);
// AC2: reviewer instances participate via default_reviewers — same path.
r = run({ args: '5', defaultReviewers: '["opencode-deepseek","opencode-mimo"]' });
assert.ok(/REVIEWER_FLAGS=\[\s*\]/.test(r), `instance default → REVIEWER_FLAGS empty, got: "${r}"`);
// AC3: bare invocation with default_reviewers unset → --codex fallback preserved.
r = run({ args: '5', defaultReviewers: '' });
assert.ok(/REVIEWER_FLAGS=\[--codex\]/.test(r), `unset default → --codex fallback, got: "${r}"`);
// AC3 defensive: empty-array default → --codex fallback (schema rejects this, but be safe).
r = run({ args: '5', defaultReviewers: '[]' });
assert.ok(/REVIEWER_FLAGS=\[--codex\]/.test(r), `empty-array default → --codex fallback, got: "${r}"`);
// AC5 (out of scope but must not regress): explicit --gemini overrides configured default.
r = run({ args: '5 --gemini', defaultReviewers: '["claude"]' });
assert.ok(/REVIEWER_FLAGS=\[.*--gemini.*\]/.test(r), `explicit flag wins over configured default, got: "${r}"`);
});
// Property test — CLAUDE.md mandates at least one fast-check (fc) property
// test for parsers and bijective contracts. The resolution block parses the
// configured review.default_reviewers JSON and classifies it into one of two
// outcomes: "non-empty array → delegate to gsd-review" (REVIEWER_FLAGS empty)
// or "anything else → fall back to --codex" (defensive — schema rejects most
// of these at config-set time, but corruption/edge cases must not crash or
// misclassify). Locks the contract so a future change can't subtly narrow or
// widen the accepted shape.
const fc = require('fast-check');
test('[property] resolution classifies arbitrary JSON values: non-empty array → empty flags, anything else → --codex fallback', (t) => {
if (process.platform === 'win32') { t.skip('POSIX shell extraction; not run on Windows'); return; }
if (!jqAvailable) { t.skip('jq not on PATH — workflow resolution block pipes through jq; structural tests above still validate the fix'); return; }
// Re-extract the blocks (the test above proved extraction works; we re-use
// the same logic rather than promoting to a helper to keep the test scope local).
const parseStart = workflow.indexOf('REVIEWER_FLAGS=""');
const parseEndMarker = "echo \"$ARGUMENTS\" | grep -q '\\-\\-all' && REVIEWER_FLAGS=\"$REVIEWER_FLAGS --all\"";
const parseEnd = workflow.indexOf(parseEndMarker);
const parseBlock = workflow.slice(parseStart, parseEnd + parseEndMarker.length);
const configGateIdx = workflow.indexOf('CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence');
const resolutionStart = workflow.indexOf('if [ -z "$REVIEWER_FLAGS" ]; then', configGateIdx);
const closingFence = workflow.indexOf('\n```\n', resolutionStart);
const resolutionBlock = workflow.slice(resolutionStart, closingFence);
const { execFileSync } = require('node:child_process');
const run = ({ args, defaultReviewers }) => {
const stub = `gsd_run() { case "$*" in *"config-get review.default_reviewers"*) printf '%s' "$GSD_TEST_DEFAULT_REVIEWERS";; *) return 0;; esac; }`;
const script = `${stub}\n${parseBlock}\n${resolutionBlock}\nprintf 'REVIEWER_FLAGS=[%s] REVIEWER_DISPLAY=[%s]' "$REVIEWER_FLAGS" "$REVIEWER_DISPLAY"`;
return execFileSync('bash', ['-c', script], {
env: { ...process.env, ARGUMENTS: args, GSD_TEST_DEFAULT_REVIEWERS: defaultReviewers ?? '' },
encoding: 'utf8',
timeout: 5000,
});
};
// Slug pattern mirrors the schema (ADR-0011: ^[a-zA-Z0-9_-]+$).
const slug = fc.stringMatching(/^[a-zA-Z0-9_-]{1,8}$/);
// Non-empty arrays of slugs → MUST classify as "use default" (REVIEWER_FLAGS empty).
const nonEmptyArray = fc.array(slug, { minLength: 1, maxLength: 4 }).map((a) => JSON.stringify(a));
// Defensive-corpus: empty array, scalar JSON, malformed JSON. All MUST fall
// back to --codex. The schema rejects the first two at config-set time, but
// a corruption/typo landing in the file directly would still reach this code.
const emptyArray = fc.constant('[]');
const scalarJson = fc.oneof(
fc.string({ maxLength: 8 }).filter((s) => !s.includes('"')).map((s) => JSON.stringify(s)),
fc.integer({ min: -10, max: 10 }).map((n) => JSON.stringify(n)),
fc.constant('null'),
fc.constant('true'),
fc.constant('false')
);
const malformedJson = fc.oneof(
fc.constant('[unclosed'),
fc.constant('{bad json'),
fc.constant('not json at all'),
fc.constant('{"k":'),
fc.string({ maxLength: 12 }).filter((s) => {
try { JSON.parse(s); return false; } catch { return true; }
})
);
// Property A: any non-empty array of slugs → empty REVIEWER_FLAGS.
fc.assert(
fc.property(nonEmptyArray, (dr) => /REVIEWER_FLAGS=\[\s*\]/.test(run({ args: '5', defaultReviewers: dr }))),
{ numRuns: 25 }
);
// Property B: anything in the defensive corpus → --codex fallback.
fc.assert(
fc.property(fc.oneof(emptyArray, scalarJson, malformedJson), (dr) => /REVIEWER_FLAGS=\[--codex\]/.test(run({ args: '5', defaultReviewers: dr }))),
{ numRuns: 25 }
);
});
});
// ─── Workflow: initialization ──────────────────────────────────────────────
describe('plan-review-convergence workflow: initialization (#2306)', () => {

View File

@@ -54,7 +54,7 @@
"pause-work.md": 14441,
"plan-milestone-gaps.md": 11809,
"plan-phase.md": 94391,
"plan-review-convergence.md": 23713,
"plan-review-convergence.md": 26285,
"plant-seed.md": 11785,
"pr-branch.md": 15963,
"profile-user.md": 21246,