* chore(#2794): single-source the reviewer invocation contract Phase 1 of epic #2782 (ADR-2782). Introduces one core descriptor table as the declared contract for all 11 cross-AI reviewer lanes, and the DEFECT.GENERATIVE-FIX parity assertion the roster has never had. The lane contract lived in three unrelated surfaces — the roster, ~640 lines of hand-authored per-CLI bash in invoke_reviewers, and the write_reviews section headings — so cross-cutting fixes landed per-leg (#2494 and #2605 were the same empty-output defect filed twice). - src/review-lane-descriptor.cts: frozen table declaring per lane the slug, flags, probe, invoke shape, timeout floor, empty-output policy, REVIEWS.md section, evidence class, required binaries, prompt-budget key and handler. Field names track ADR-2782 D1/D2/D6/D7 verbatim so Phase 2 harvests the shape with no translation layer. It declares; it does not execute — invoke_reviewers iterates in Phase 5b. - checkReviewerLaneParity: bidirectional parity across descriptor, roster, invoke_reviewers legs and write_reviews sections. Forward-only would miss the failure it exists to catch (#2718 added a leg, #2781 was the drift). ADR-1517 instance headings are exempt per D8. - Legs carry an explicit <!-- reviewer-lane: slug --> marker; five non-lane bold labels share the bold-then-fence shape a heuristic matcher would key on. - ADR-2782 D4: an explicitly-flagged reviewer that cannot run is now an error in both the core module and the workflow prose that mirrors it. A code-only change would be unobservable — the module has no production caller; the workflow narrates the policy. Discovery paths (--all, review.default_reviewers) stay lenient. - Fixes the qwen leg, the last one discarding stderr to /dev/null. Two ADR-2782 D2 vocabulary widenings were forced by surveying the shipped legs: promptChannel 'none' (CodeRabbit is fed no prompt) and outputChannel 'file-arg' (Codex writes via -o and discards stdout, #1698). Both are additive and closed; Phase 2 owns the validator. Closes #2690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2794): make the parity checker total and pin the lane slug grammar Findings from the orthogonal review passes. Spec axis — the module claimed its vocabulary tracked ADR-2782 D1/D2 "verbatim" while diverging in three undisclosed ways, which is the translation layer Phase 2 was supposed to be spared: - `transport` moves from `invoke.transport` to the LANE level, a sibling of `probe`/`invoke`, exactly as D1's manifest example places it. The nested form read better as a TS discriminated union; the union is now discriminated at the lane level instead, which costs nothing. - The header and the CONTEXT.md glossary now enumerate all FOUR widenings (adding `outputArg` and `flags[]`), not two. Standards axis — CLAUDE.md requires a fast-check property test for a parser, and `checkReviewerLaneParity` parses markdown for markers and headings. Adding one found two real defects that the hand-written matrix missed: - NOT TOTAL: a malformed descriptor entry threw on `lane.flags` iteration, contradicting the module's own "never throws" claim. Every field is now narrowed from `unknown` at the trust boundary and reported as MALFORMED_LANE / INVALID_SLUG. This matters because Phase 2 feeds this function third-party overlay data, and a parity gate that crashes is indistinguishable from one never run. - SILENT GRAMMAR MISMATCH: LEG_MARKER_RE captures only [a-z0-9_-], so a slug outside that class was unmatchable — its marker could be present and correct and the scan would still report LEG_MARKER_MISSING forever. LANE_SLUG_RE now pins the grammar and a violating slug is reported INVALID_SLUG. A loud named violation beats a silent miss. Generators are document-shaped, not writer-seeded (CONTRIBUTING #2371): seeding from the module's own matchers could only produce documents those matchers already recognize. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2794): register the new bin/lib module in the ESLint ignore list The remote runner caught this; lint:ci did not, because the invariant lives in the test suite rather than the lint chain: tests/repo-invariants.test.cjs "each bin/lib/*.cjs is linted xor ignored according to migration state" -> tsc-generated bin/lib modules not yet added to ESLint ignore list: review-lane-descriptor.cjs Adding a src/*.cts module ripples to six surfaces (.gitignore, the ESLint ignore list, docs/INVENTORY-MANIFEST.json, the CONTEXT.md glossary, the capability/inventory manifests, and any size baseline). The other five were covered; this was the miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(#2794): amend ADR-2782 D1/D2/D8 with the vocabulary Phase 1 surfaced Building the Phase 1 descriptor table against all eleven shipped legs is the first time every lane's contract was written in one place, and it surfaced four cases the ADR's original survey did not cover. Amending the design lock rather than diverging from it, so Phase 2 (#2795) implements the manifest validator against the amended vocabulary instead of rediscovering the gaps. All four are additive widenings of closed enums; no decision reverses: - D2 promptChannel gains `none` — coderabbit is fed no prompt at all, it reviews the working-tree diff. - D2 outputChannel gains `file-arg` — the ADR called a file-writing lane a shape a real CLI *could* take; codex already is one, writing via -o/--output-last-message and discarding stdout (#1698). - D2 gains `outputArg`, required iff file-arg — knowing the review lands in a file is useless without the argument naming it. - D1 `flag` becomes `flags[]` and D8's uniqueness flattens across lanes — antigravity is selected by both --antigravity and --agy, which a single-valued field cannot express. This is the same evidence path that produced the openai-http transport: the vocabulary widens on a lane that exists, under review, never on speculation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2794): backfill changeset pr number to 2820 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,9 +50,11 @@ command -v jq >/dev/null 2>&1 && echo "jq:available" || echo "jq:missing"
|
||||
**jq-dependent reviewer lanes.** `jq` is a production prerequisite for the
|
||||
`ollama`, `lm_studio`, `llama_cpp`, `opencode`, and `antigravity` lanes only. If
|
||||
`detect_clis` reports `jq:missing`, treat those five as **undetected** — they
|
||||
follow the same "known-but-undetected" path as a missing CLI (info note, ignored;
|
||||
and if they were the only selected reviewers, fail with the actionable message
|
||||
below rather than producing an empty review). Tell the user to install jq:
|
||||
follow the same "known-but-undetected" path as a missing CLI. Which path that is
|
||||
depends on how the lane was selected (see the precedence rules below): reached
|
||||
through `review.default_reviewers` or `--all` it is an info note and the lane is
|
||||
ignored; named by an explicit flag it is an **error**, because the user asserted
|
||||
that lane. Tell the user to install jq:
|
||||
|
||||
```
|
||||
NOTE: jq is not on PATH — the ollama, lm_studio, llama_cpp, opencode, and
|
||||
@@ -85,10 +87,22 @@ Reviewer-selection precedence:
|
||||
3. `review.default_reviewers`
|
||||
4. No key + no flags → all detected reviewers
|
||||
|
||||
**Explicit reviewer flags are an assertion, not a preference (ADR-2782 D4).** A lane the user
|
||||
named on the command line and that cannot run is an **error**, surfaced and non-silent — even
|
||||
when other named lanes did run. Do not proceed with a thinner reviewer set and report success:
|
||||
`--gemini --qwen` on a host without `qwen` fails, it does not quietly become a Gemini-only
|
||||
review. This applies however the lane became unavailable — binary missing, prerequisite `jq`
|
||||
absent, or a local server not reachable.
|
||||
|
||||
The asymmetry is deliberate: *not finding a lane nobody asked for is normal; failing to run a
|
||||
lane somebody asked for is an error.* A user who wants "whatever is available" has `--all`; a
|
||||
user who wants a preferred set has `review.default_reviewers`. Both stay lenient below.
|
||||
|
||||
`review.default_reviewers` behavior:
|
||||
- Value must be a non-empty array of slug strings (configured via `gsd config-set review.default_reviewers '["gemini","codex"]'`)
|
||||
- Unknown slugs warn and are ignored
|
||||
- Known-but-undetected slugs emit an info note and are ignored
|
||||
- Known-but-undetected slugs emit an info note and are ignored — a configured default is a
|
||||
preference evaluated across many hosts, so a subset being present is expected, not an error
|
||||
- If all configured reviewers are unavailable, fail with an actionable message
|
||||
|
||||
**Reviewer instances (#1517, optional):** if `review.reviewer_instances` is configured,
|
||||
@@ -303,6 +317,7 @@ For each selected CLI, invoke in sequence (not parallel — avoid rate limits):
|
||||
|
||||
**Timeout guidance (#2194):** prompt-fed source-grounded reviews are slow — measured ~570s for Codex at `xhigh` effort and ~525s for headless Claude on a large plan set. Each of the Gemini / Claude / Codex blocks below MUST be invoked with a high Bash `timeout:` — at least `900000` (15 min), and `1200000` (20 min) for Codex `xhigh` or headless Claude — so a lane is not killed mid-review. On Claude Code, raise the host cap via `BASH_MAX_TIMEOUT_MS` if a review can exceed it. A silent empty output after a long run is a **timeout kill, not a crash** — the Codex `0xc0000142` misdiagnosis persisted because the empty-output branches below cannot distinguish the two; treat an empty result on a slow lane as a dropped lane and re-run with more time rather than diagnosing a CLI/sandbox failure. A cross-AI review that silently drops a lane is blind in one eye.
|
||||
|
||||
<!-- reviewer-lane: gemini -->
|
||||
**Gemini:**
|
||||
```bash
|
||||
# #2494: capture stderr to a .err sidecar (not /dev/null) and stub an empty
|
||||
@@ -326,6 +341,7 @@ if [ ! -s {run_dir}/gsd-review-gemini.md ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: claude -->
|
||||
**Claude (separate session):**
|
||||
```bash
|
||||
# #2494: same guard as the Gemini block above — stderr to a .err sidecar
|
||||
@@ -341,6 +357,7 @@ if [ ! -s {run_dir}/gsd-review-claude.md ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: codex -->
|
||||
**Codex:**
|
||||
```bash
|
||||
# No hook-trust bypass flag — see the #2479 note above. Capture stderr to a .err
|
||||
@@ -362,6 +379,7 @@ if [ ! -s {run_dir}/gsd-review-codex.md ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: coderabbit -->
|
||||
**CodeRabbit:**
|
||||
|
||||
Note: CodeRabbit reviews the current git diff/working tree — it does not accept a prompt or model flag. It may take up to 5 minutes. Use `timeout: 360000` on the Bash tool call. The source-grounding requirement in the build_prompt Review Instructions applies only to the prompt-fed reviewers above; CodeRabbit is a diff-only reviewer and never receives it. Treat its output as a diff observation, not a grounded plan-level verdict.
|
||||
@@ -378,6 +396,7 @@ if [ ! -s {run_dir}/gsd-review-coderabbit.md ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: opencode -->
|
||||
**OpenCode (via GitHub Copilot):**
|
||||
|
||||
OpenCode's default `build` agent is an agentic coder, not a prompt→completion API.
|
||||
@@ -426,14 +445,22 @@ else
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: qwen -->
|
||||
**Qwen Code:**
|
||||
```bash
|
||||
cat {run_dir}/gsd-review-prompt.md | qwen - 2>/dev/null > {run_dir}/gsd-review-qwen.md
|
||||
# #2794: the last leg still sending stderr to /dev/null. Every other lane
|
||||
# captures it to a .err sidecar and appends it to the stub (#2494/#2605); qwen
|
||||
# wrote a bare "failed or returned empty output." with no diagnostic at all, so
|
||||
# a missing binary, an auth prompt, and a rate-limit were indistinguishable from
|
||||
# each other and from a clean empty review.
|
||||
cat {run_dir}/gsd-review-prompt.md | qwen - 2>{run_dir}/gsd-review-qwen.err > {run_dir}/gsd-review-qwen.md
|
||||
if [ ! -s {run_dir}/gsd-review-qwen.md ]; then
|
||||
echo "Qwen review failed or returned empty output." > {run_dir}/gsd-review-qwen.md
|
||||
echo "Qwen review failed or returned empty output. stderr:" > {run_dir}/gsd-review-qwen.md
|
||||
cat {run_dir}/gsd-review-qwen.err >> {run_dir}/gsd-review-qwen.md 2>/dev/null
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: cursor -->
|
||||
**Cursor:**
|
||||
```bash
|
||||
# cursor-agent is a SEPARATE binary from the `cursor` IDE launcher; print mode (-p) takes the
|
||||
@@ -453,6 +480,7 @@ if [ ! -s {run_dir}/gsd-review-cursor.md ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: antigravity -->
|
||||
**Antigravity CLI:**
|
||||
|
||||
**Maintainer note — why this block has three layers (last updated against agy 1.0.16):**
|
||||
@@ -638,6 +666,7 @@ if [ -s {run_dir}/gsd-review-antigravity.md ] && \
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: ollama -->
|
||||
**Ollama (local, OpenAI-compatible):**
|
||||
|
||||
Read host and model from config. All three local backends share the same `/v1/chat/completions` endpoint — only host and model differ. Use `jq --rawfile` to safely encode the multi-line prompt as JSON without shell-escaping issues.
|
||||
@@ -741,6 +770,7 @@ echo "Ollama review skipped: prompt budget (${OLLAMA_REVIEWER_BUDGET} tokens) to
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: lm_studio -->
|
||||
**LM Studio (local, OpenAI-compatible):**
|
||||
```bash
|
||||
# Resolve prompt budget for LM Studio: per-reviewer override > global default > null
|
||||
@@ -826,6 +856,7 @@ echo "LM Studio review skipped: prompt budget (${LM_STUDIO_REVIEWER_BUDGET} toke
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: llama_cpp -->
|
||||
**llama.cpp (local, OpenAI-compatible):**
|
||||
```bash
|
||||
# Resolve prompt budget for llama.cpp: per-reviewer override > global default > null
|
||||
|
||||
Reference in New Issue
Block a user