* chore(#2799): resolve reviewer lanes into executable invocation plans Phase 5b of ADR-2782. Adds the resolver and runner that let invoke_reviewers iterate declared lanes instead of hand-authored per-CLI bash. Five additive descriptor amendments, each forced by a lane that ships today: - LaneHandler gains 'opencode' — the lane rebuilds its review from assistant text parts of a --format json stream; a plain stdout copy re-breaks #1936. - modelConfigKey — antigravity's key is review.models.agy, not .antigravity, so resolving by slug silently dropped a configured model. - defaultHost/fallbackModel — Phase 4 federated every *_host with a default of empty string; the real fallback only existed in the bash. - args becomes an argv template with a closed four-placeholder vocabulary. Positional splicing produced 'codex --model M -o F exec --ephemeral', which is not a valid invocation: codex injects in the middle, twice. - kimi-code lane, with the bounded command-capability probe (needle --output-format) that tells Kimi Code from the legacy python kimi-cli. Parity gate re-pointed: the workflow-text families it scanned are the text this phase deletes, so they are replaced by descriptor-to-registry parity plus an anti-parity check that no bespoke leg returns. jq, curl and external timeout/gtimeout all drop out of the review path. Refs #2782 * chore(#2799): add review-lane query surface and widen the manifest vocabulary Adds the gsd-tools 'review-lane' route (plan/invoke/sections) the workflow loops over, projects all twelve lanes into their capability manifests, and widens capability-validator for the amendments. opencode admitted to VALID_LANE_HANDLERS under the second arm of the enum's own admission rule: one lane, justified by a documented upstream defect data cannot express (#1936 — the agent can end its turn with zero output tokens and --format default then drops the assistant text entirely). Two bugs caught by an end-to-end stub run and fixed here: - loadConfigResolved returns a provenance wrapper, not the config; using it directly resolved every key to undefined, which reads as 'nothing configured' and silently dropped every model override. - hasBinary used shell:true with an args array (Node 26 DEP0190). Replaced with a PATH scan that spawns nothing at all. Refs #2782 * chore(#2799): iterate declared lanes in invoke_reviewers and write_reviews Replaces the eleven hand-authored per-CLI bash legs with a loop over resolved lanes, and renders REVIEWS.md sections from each lane's declared reviewsSection instead of thirteen hardcoded headings. review.md drops from 1104 lines to 507 (61KB to 28.7KB). Parity gate re-pointed, as agreed: the leg-marker and section-heading families scanned exactly the text this phase deletes, so they are replaced by descriptor-to-registry parity in both directions, plus an anti-parity check that fires if a bespoke leg is ever re-added. Enum, emitting sites and the Object.keys lock moved together. The budget-trim helper is hoisted out of the Ollama leg: it was always lane-agnostic, and any lane may now declare a promptBudgetKey. Refs #2782 * feat(#2799): bind the consented egress host and re-verify it at invocation Completes ADR-2782 D5. Rule 1 was recorded in the ADR as delivered by Phase 3 but was not implemented: ConsentRecord had no host field and nothing in the tree bound one, so this phase's rule-4 comparison had no baseline. ConsentRecord gains an OPTIONAL reviewerHost. Optional is the whole design: isValidConsentRecord does not require it, so every record already on disk stays valid and no re-consent storm fires (D4 rule 5). It is deliberately excluded from disclosureSignature — the loader has no config resolver, so folding a config-derived value in would make loader and lifecycle compute different signatures for the same manifest and re-prompt forever. Install resolves hostConfigKey (falling back to the lane's declared defaultHost, which is what the invocation path uses) and records it. Invocation re-resolves and blocks on mismatch rather than silently redirecting. Absence allows: no record, or a record predating the field, means nothing to compare — denying there would break every existing local-model user on upgrade. Refs #2782 * test(#2799): cover the resolver, runner and handlers; retarget the parity suites Adds the golden invocation-plan table (one row per shipped lane, derived from the bash legs rather than the descriptor types) plus runner coverage for the probe, empty-output policy, the three handlers and the egress check. Retargets the existing suites onto the new contract: descriptor-to-registry parity, the anti-parity check, the opencode handler, and the twelfth lane. Two corrections found by running them: - modelConfigKey was required; that breaks D4 rule 2, since a reviewer manifest authored before this phase would fail validation on upgrade. It is optional, read as null when absent. - the antigravity non-zero-exit test pre-seeded the transcript, which asserted that a STALE entry leaks through — the exact bug the watermark prevents. The spawn now appends, as the real tool does. Refs #2782 * fix(#2799): restore agy --add-dir and the self-report prompt in the handler Retargeting the three legacy reviewer suites off the deleted bash surfaced two real regressions in the port, both #2176: - --add-dir was dropped. Without it agy's permission context never receives the cwd repo, so the agent anchors on its own scratch dir and reviews the plan text in isolation — the exact failure the Review Instructions forbid. It is capability-probed, because an older agy rejects the unknown flag outright and a lane that fails to start is worse than one running on the prompt anchor. - the prompt lost the clause mandating a REVIEWED-WITHOUT-REPO-ACCESS self-report, which is what makes a blind review distinguishable from a grounded one. antigravity now builds its own prompt variant. Also ports the #2073 mode-2 cli.log diagnostic, which was dropped: a pinned model that 404s exits 0 with empty stdout AND an empty transcript, so agy's own log is the only evidence that anything failed. The three suites now assert against the plan and the handler instead of matching fence text, so they no longer need allow-test-rule exemptions. Refs #2782 * docs(#2799): document the declared lanes, the new flag, and dropped prerequisites COMMANDS.md gains --kimi-code and replaces the jq-prerequisite paragraph, which is now false: no lane requires jq, curl or an external timeout. Adds the changed-egress-destination behavior, since a blocked lane is something a user can hit. CONFIGURATION.md records that the model config key is declared per lane rather than derived from the flag — antigravity's is review.models.agy — and adds review.models.kimi-code. reviewer-instances.md now routes an instance through its lane's single invocation seam instead of a copied per-adapter bash block, which is what lets a cross-cutting fix reach instances for free. That required implementing the --model/--agent/--as flags it documents; --model re-resolves through the lane's argv template rather than splicing, so the flag lands where the lane declares it rather than ahead of a subcommand. CONTEXT.md glossary gains both new modules. Refs #2782 * chore(#2799): drop the stale emitted-drift acknowledgment The only entry was #2797's, acknowledging COMMENT-ONLY GROWTH in review.md. That file now shrinks by ~32KB and every emitted hash that moved is attributable to this diff, so the ack no longer explains anything. Removing the last entry means removing the file: its presence is the alarm, and an empty one signals nothing. Verified by deleting it and re-running the attribution and provenance gates plus lint:ci — all green without it. Refs #2782 * docs(#2799): record the Phase 5b vocabulary widenings in ADR-2782 Five additive amendments, each forced by a lane that ships today, plus two corrections the phase had to make rather than work around: - D5 rule 1 was recorded as delivered by Phase 3 and was not implemented, so this phase's rule-4 comparison had no baseline. Recorded because an ADR asserting a rule was delivered is exactly what stops a later phase checking. - The DEFECT.GENERATIVE-FIX gate is re-pointed: its workflow-text families scanned the text this phase deletes. Also records that D7's 'skip the probe where no bounding mechanism exists' carve-out is obsolete — in practice it meant the Antigravity lane ran unbounded on every stock macOS host, which ships neither timeout nor gtimeout. Refs #2782 * fix(#2799): close four defects found by adversarial review Two confirmed bugs, both reproduced before fixing: - resolveLanePlan was not total. An openai-http lane with a missing or non-object invoke dereferenced inv.hostConfigKey and threw, contradicting the module's own documented contract; the spawn branch guarded correctly and the http branch did not. The CLI seam resolves every selected lane in one map, so one malformed overlay manifest would have aborted the whole review rather than dropping its own lane. Guarded, plus a per-lane try/catch at the seam so a throw can never take down siblings. - A reviewer-instance model was silently dropped for any lane declaring modelConfigKey null (cursor, qwen, coderabbit). reviewer_instances validates that cli is a known slug but never that the slug accepts a model, so a user could configure one, get a clean run, and never learn a different model reviewed their plan. Now warns explicitly. Two hardening fixes: - The slug is concatenated into artifact paths, so LANE_SLUG_RE is enforced in the resolver rather than inherited from a validator that does not run on this path — the module documents itself as the overlay-manifest trust boundary, so it should not depend on someone else having checked. - normalizeHost mangled a scheme-less value: new URL('localhost:11434') parses with an empty hostname, so it became 'localhost://11434' and was compared and requested as if real. An empty hostname now means not-a-URL. Also documents the one gap that cannot be closed here: the antigravity watermark is keyed by workspace, so two concurrent reviews of the same repo share a transcript. agy exposes no per-invocation id to filter on, so the handler now states which half of its never-stale guarantee actually holds. Refs #2782 * test(#2799): retarget the remaining eight review.md-asserting suites The remote runner found 37 failures the local sweep missed (it hit the shell's two-minute cap before reaching these). All eight extract per-CLI bash from review.md that this phase deletes; each protects a real invariant, so each is retargeted onto the plan, the runner or the handler rather than removed. Three real defects surfaced by doing so: - effort args never reached ANY lane. model-resolver.cjs exports no resolveExecution, so effortFor silently returned [] every time. Restored by calling the same bounded resolve-execution query the bash legs used — and NOT with --raw, which prints the resolved effort rather than the picked field, so claude got 'low' instead of '--effort low'. - the timeout guidance lost 'a silent empty output is a timeout kill, not a crash' — the operator note that exists because of the Codex 0xc0000142 misdiagnosis. Restored. - the opencode handler dropped EMPTY assistant text parts. The shipped jq was , and only substitutes for false/null — an empty string is truthy in jq and contributed a blank line. Found by a property test shrinking to ['', '']. The opencode property suite no longer spawns jq at all, which deletes the #2099 hang mechanism it was architected around rather than mitigating it. Refs #2782 * fix(#2799): register the two new generated modules, and untrack them The remote runner caught build output committed to git. Both new modules compile from src/*.cts into gsd-core/bin/lib/*.cjs, and every sibling generated that way is gitignored and eslint-ignored (ADR-457) - including Phase 1's own review-lane-descriptor.cjs. Mine were neither, so repo-invariants' "each bin/lib/*.cjs is linted xor ignored according to migration state" failed. Registered both in .gitignore and eslint.config.mjs alongside the Phase 1 module, and dropped them from the index. Nothing about the shipped behaviour changes; the artifacts are rebuilt by build:lib. This is the new-.cts-module registration ripple, and it is the one part of it I had not completed - the CONTEXT.md glossary and the inventory manifest were already done. Refs #2782 * chore(#2799): backfill changeset pr number to 2861 * chore(#2799): backfill changeset pr number to 2861 --------- Co-authored-by: Test <test@example.com>
This commit is contained in:
@@ -277,656 +277,121 @@ Note: `INSTRUCTIONS_BLOCK_FILE`, `ROADMAP_SECTION_FILE`, and `PHASE_DIR` come fr
|
||||
</step>
|
||||
|
||||
<step name="invoke_reviewers">
|
||||
Read model preferences from planning config. Null/missing values fall back to CLI defaults.
|
||||
Every reviewer lane is **declared data** (ADR-2782). This step iterates the lanes the selection
|
||||
resolved; it does not enumerate them. Adding a reviewer is a capability manifest, not an edit here.
|
||||
|
||||
**Do not re-add a per-CLI block.** A `<!-- reviewer-lane: … -->` marker anywhere in this step now
|
||||
FAILS the parity gate (`checkReviewerLaneParity` → `bespoke_leg_present`). Lane divergence is
|
||||
declared in the manifest — timeout floor, probe, prompt/output channel, empty-output policy — and
|
||||
behaviour that data genuinely cannot express is a named first-party `handler` (ADR-2782 D6), never
|
||||
a bespoke block here.
|
||||
|
||||
**Timeout guidance (#2194):** prompt-fed source-grounded reviews are slow — measured ~570 s for
|
||||
Codex at `xhigh` effort and ~525 s for headless Claude on a large plan set. Each lane declares its
|
||||
own `timeoutFloorMs` and the runner enforces it internally, but the **Bash tool call wrapping the
|
||||
loop below must still be given a high `timeout:`** — at least `900000`, and `1200000` when Codex or
|
||||
headless Claude are in the selection — or the host kills the whole loop mid-lane. 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 for exactly this reason, because an empty result cannot distinguish the two
|
||||
on its own. Treat an empty result on a slow lane as a dropped lane and re-run with more time rather
|
||||
than diagnosing a CLI or sandbox failure. A cross-AI review that silently drops a lane is blind in
|
||||
one eye.
|
||||
|
||||
**No hook-trust bypass (#2479):** no lane passes a hook-trust bypass flag and none runs a capability
|
||||
probe for one. That flag only bypasses *persisted* hook trust (a first-run condition) and flagless
|
||||
invocations work in steady state, while host-harness safety classifiers deny commands carrying it.
|
||||
An environment that genuinely hits an untrusted-hook prompt surfaces through the `.err` capture and
|
||||
the empty-output stub as a dropped lane with diagnosable stderr, not silent attrition. Do not
|
||||
reintroduce the flag (even spelled out in prose — a regression test bans the literal file-wide).
|
||||
|
||||
**Reviewer instances (#1517, optional):** instances resolve *through* a lane and are not lanes
|
||||
themselves (ADR-2782 D8). Each selected instance invokes its base `cli` with its own `model`/`agent`
|
||||
as opaque argv. Exact invocation in `gsd-core/references/reviewer-instances.md`.
|
||||
|
||||
Lanes run **sequentially, not in parallel** — concurrent invocation trips provider rate limits.
|
||||
|
||||
```bash
|
||||
# JSON scalars from gsd-tools.cjs query; --raw strips the JSON quotes natively
|
||||
# (no jq dependency — jq is absent on stock Windows/Git-Bash, #2589)
|
||||
GEMINI_MODEL=$(gsd_run query config-get review.models.gemini --raw 2>/dev/null || true)
|
||||
CLAUDE_MODEL=$(gsd_run query config-get review.models.claude --raw 2>/dev/null || true)
|
||||
CODEX_MODEL=$(gsd_run query config-get review.models.codex --raw 2>/dev/null || true)
|
||||
OPENCODE_MODEL=$(gsd_run query config-get review.models.opencode --raw 2>/dev/null || true)
|
||||
# review.models.agy, when set, is passed to agy as --model (escape hatch for a
|
||||
# pinned model that 404s server-side); otherwise agy uses its persisted default.
|
||||
AGY_MODEL=$(gsd_run query config-get review.models.agy --raw 2>/dev/null || true)
|
||||
RUN_DIR="{run_dir}"
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
# SELECTED_REVIEWERS is the comma-separated result of reviewer selection (ADR-0011 precedence:
|
||||
# explicit flags > --all > review.default_reviewers > all detected). Unchanged by this phase.
|
||||
|
||||
# Reasoning effort per reviewer (#2481). Empty unless the host's effortSurface
|
||||
# axis is `argv`. Pass --attempt N to walk ADR-443's escalation ladder.
|
||||
CLAUDE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host claude --pick effort_argv_string 2>/dev/null || true)
|
||||
CODEX_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host codex --pick effort_argv_string 2>/dev/null || true)
|
||||
OPENCODE_EFFORT_ARGS=$(gsd_run query resolve-execution gsd-plan-checker --host opencode --pick effort_argv_string 2>/dev/null || true)
|
||||
```
|
||||
|
||||
**No hook-trust bypass (#2479):** the codex invocations below deliberately pass no
|
||||
hook-trust bypass flag and run no capability probe for one (#1115's former gate).
|
||||
That flag only bypasses *persisted* hook trust (a first-run condition) and flagless
|
||||
invocations work in steady state, while host-harness safety classifiers deny
|
||||
commands carrying it — and cited the probe itself as intent (#2479). An environment
|
||||
that genuinely hits an untrusted-hook prompt surfaces through the `.err` capture +
|
||||
empty-output guard below as a dropped lane with diagnosable stderr, not silent
|
||||
attrition. Do not reintroduce the flag (even spelled out in prose here — a
|
||||
regression test bans the literal file-wide) or the probe; the #1115 "unexpected
|
||||
argument" failure mode existed only because the flag was emitted, so with no flag
|
||||
there is nothing to version-gate.
|
||||
|
||||
**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):
|
||||
|
||||
**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
|
||||
# result, mirroring the Codex and Cursor blocks. Without the guard a failed
|
||||
# lane — CLI missing, unauthenticated, rate-limited, crashed, or any exit that
|
||||
# writes no stdout — leaves a zero-byte file that write_reviews renders as a
|
||||
# reviewer that ran cleanly with nothing to report, silently dropping a lane
|
||||
# from the cross-AI consensus while present_results reports success.
|
||||
# Scope limit: the guard only runs if this block completes. A host Bash-tool
|
||||
# timeout that kills the whole block skips it, the same hard bound the
|
||||
# OpenCode block documents below — per the timeout guidance above, treat an
|
||||
# empty result on a slow lane as a dropped lane rather than a crash.
|
||||
if [ -n "$GEMINI_MODEL" ] && [ "$GEMINI_MODEL" != "null" ]; then
|
||||
cat {run_dir}/gsd-review-prompt.md | gemini -m "$GEMINI_MODEL" -p - 2>{run_dir}/gsd-review-gemini.err > {run_dir}/gsd-review-gemini.md
|
||||
else
|
||||
cat {run_dir}/gsd-review-prompt.md | gemini -p - 2>{run_dir}/gsd-review-gemini.err > {run_dir}/gsd-review-gemini.md
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-gemini.md ]; then
|
||||
echo "Gemini review failed or returned empty output. stderr:" > {run_dir}/gsd-review-gemini.md
|
||||
cat {run_dir}/gsd-review-gemini.err >> {run_dir}/gsd-review-gemini.md
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: claude -->
|
||||
**Claude (separate session):**
|
||||
```bash
|
||||
# #2494: same guard as the Gemini block above — stderr to a .err sidecar
|
||||
# instead of /dev/null, and a diagnostic stub when the lane produces nothing.
|
||||
if [ -n "$CLAUDE_MODEL" ] && [ "$CLAUDE_MODEL" != "null" ]; then
|
||||
cat {run_dir}/gsd-review-prompt.md | claude --model "$CLAUDE_MODEL" $CLAUDE_EFFORT_ARGS -p - 2>{run_dir}/gsd-review-claude.err > {run_dir}/gsd-review-claude.md
|
||||
else
|
||||
cat {run_dir}/gsd-review-prompt.md | claude $CLAUDE_EFFORT_ARGS -p - 2>{run_dir}/gsd-review-claude.err > {run_dir}/gsd-review-claude.md
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-claude.md ]; then
|
||||
echo "Claude review failed or returned empty output. stderr:" > {run_dir}/gsd-review-claude.md
|
||||
cat {run_dir}/gsd-review-claude.err >> {run_dir}/gsd-review-claude.md
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: codex -->
|
||||
**Codex:**
|
||||
```bash
|
||||
# No hook-trust bypass flag — see the #2479 note above. Capture stderr to a .err
|
||||
# file (not /dev/null) so a non-zero exit — e.g. an untrusted-hook prompt on a
|
||||
# first run — is diagnosable instead of a silent empty review (#1115).
|
||||
# Capture the review via codex's own `-o/--output-last-message <FILE>` (only the
|
||||
# final agent message) and discard stdout (#1698): on some platforms (Windows)
|
||||
# codex writes process-teardown output to stdout *after* the final message, and a
|
||||
# stdout redirect would append that noise to a non-empty file — slipping past the
|
||||
# `[ ! -s … ]` empty-output guard as a silently polluted review.
|
||||
if [ -n "$CODEX_MODEL" ] && [ "$CODEX_MODEL" != "null" ]; then
|
||||
cat {run_dir}/gsd-review-prompt.md | codex exec --ephemeral --model "$CODEX_MODEL" $CODEX_EFFORT_ARGS --skip-git-repo-check -o {run_dir}/gsd-review-codex.md - 2>{run_dir}/gsd-review-codex.err >/dev/null
|
||||
else
|
||||
cat {run_dir}/gsd-review-prompt.md | codex exec --ephemeral $CODEX_EFFORT_ARGS --skip-git-repo-check -o {run_dir}/gsd-review-codex.md - 2>{run_dir}/gsd-review-codex.err >/dev/null
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-codex.md ]; then
|
||||
echo "Codex review failed or returned empty output. stderr:" > {run_dir}/gsd-review-codex.md
|
||||
cat {run_dir}/gsd-review-codex.err >> {run_dir}/gsd-review-codex.md
|
||||
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.
|
||||
|
||||
```bash
|
||||
# #2605: same guard as every other leg (#2494/#2592). `2>/dev/null` with no
|
||||
# `[ ! -s … ]` stub left a zero-byte file when coderabbit was missing,
|
||||
# unauthenticated, or exited without stdout — write_reviews then rendered a
|
||||
# reviewer that "ran cleanly with nothing to report", silently dropping the lane.
|
||||
coderabbit review --prompt-only 2>{run_dir}/gsd-review-coderabbit.err > {run_dir}/gsd-review-coderabbit.md
|
||||
if [ ! -s {run_dir}/gsd-review-coderabbit.md ]; then
|
||||
echo "CodeRabbit review failed or returned empty output. stderr:" > {run_dir}/gsd-review-coderabbit.md
|
||||
cat {run_dir}/gsd-review-coderabbit.err >> {run_dir}/gsd-review-coderabbit.md 2>/dev/null
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: opencode -->
|
||||
**OpenCode (via GitHub Copilot):**
|
||||
|
||||
OpenCode's default `build` agent is an agentic coder, not a prompt→completion API.
|
||||
On a large review prompt it may run a few `read` tool calls and then end its turn
|
||||
with **zero output tokens** (`reason:"stop"`, `output:0`), so `--format default`
|
||||
yields empty stdout and the second reviewer is silently lost (#1936). Invoke with
|
||||
`--format json` and reconstruct the review from the assistant `text` parts; if the
|
||||
agent emitted none, surface the stop `reason`, output-token count, and captured
|
||||
stderr so the failure is diagnosable instead of a generic empty stub. Runs are also
|
||||
nondeterministic in length, so bound this Bash tool call with a wall-clock timeout —
|
||||
set `timeout: 660000` on the call (same mechanism the CodeRabbit block documents).
|
||||
That bound is hard: if it fires mid-`opencode run` the tool kills the command and
|
||||
the jq reconstruction below never runs, so the reviewing agent simply proceeds
|
||||
without an OpenCode result. The completing zero-output case — the actual #1936 bug —
|
||||
is fully handled below; the timeout only backstops the rarer nondeterministic hang. A
|
||||
reviewer instance with `"agent": "review"` (see
|
||||
`gsd-core/references/reviewer-instances.md`) sidesteps the default `build` agent and
|
||||
is the durable fix when this recurs.
|
||||
|
||||
```bash
|
||||
# stderr → sidecar (never /dev/null) so a real error is diagnosable — mirrors the
|
||||
# Codex block. --format json is the primary invocation (not a fallback): the review
|
||||
# text lives in assistant `text` parts, which the default formatter drops when the
|
||||
# agent stops with no final message (#1936).
|
||||
if [ -n "$OPENCODE_MODEL" ] && [ "$OPENCODE_MODEL" != "null" ]; then
|
||||
set -- --model "$OPENCODE_MODEL"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
cat {run_dir}/gsd-review-prompt.md | opencode run "$@" $OPENCODE_EFFORT_ARGS --format json - 2>{run_dir}/gsd-review-opencode.err > {run_dir}/gsd-review-opencode.json
|
||||
# Reconstruct the review from the assistant text parts into a variable and test
|
||||
# its CONTENT, not the file size: an empty extraction still prints a trailing
|
||||
# newline that would fool a `[ -s file ]` check into skipping the stub.
|
||||
OPENCODE_REVIEW=$(jq -rs '[.[] | select(.type=="text") | .part.text // empty] | join("\n")' {run_dir}/gsd-review-opencode.json 2>/dev/null)
|
||||
if [ -n "$OPENCODE_REVIEW" ]; then
|
||||
printf '%s\n' "$OPENCODE_REVIEW" > {run_dir}/gsd-review-opencode.md
|
||||
else
|
||||
# No assistant text (no final message, or stdout was not valid JSON):
|
||||
{
|
||||
echo "OpenCode review returned no assistant text (#1936: agent ended its turn with no final message)."
|
||||
OPENCODE_DIAG=$(jq -rs '[.[] | select(.type=="step_finish")] | last | "stop reason=\(.part.reason // "?"), output tokens=\(.part.tokens.output // "?")"' {run_dir}/gsd-review-opencode.json 2>/dev/null)
|
||||
[ -n "$OPENCODE_DIAG" ] && echo "Diagnostic: $OPENCODE_DIAG"
|
||||
echo "stderr:"
|
||||
cat {run_dir}/gsd-review-opencode.err
|
||||
} > {run_dir}/gsd-review-opencode.md
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: qwen -->
|
||||
**Qwen Code:**
|
||||
```bash
|
||||
# #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. 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
|
||||
# prompt as an ARGUMENT, not stdin. A full review prompt can exceed the OS argument limit, so
|
||||
# reference the prompt file by path rather than inlining it. Capture stderr so a failure is
|
||||
# diagnosable instead of a silent empty result.
|
||||
# #2176: same absolute-root anchor as the Antigravity block — cursor-agent runs
|
||||
# in the repo cwd, but repo-relative references in the assembled prompt still
|
||||
# need an explicit root to resolve against. rev-parse (not bare pwd) so the
|
||||
# anchor is correct even when /gsd:review is invoked from a repo subdirectory.
|
||||
_CURSOR_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
CURSOR_PROMPT_ARG="Read the file at {run_dir}/gsd-review-prompt.md in full and carry out the review request it contains. The repository under review is at $_CURSOR_ROOT — resolve every relative file path in the review request against that absolute root. Output only the resulting markdown review. Do not edit any files."
|
||||
cursor-agent -p --mode ask --trust --output-format text "$CURSOR_PROMPT_ARG" 2>{run_dir}/gsd-review-cursor.err > {run_dir}/gsd-review-cursor.md
|
||||
if [ ! -s {run_dir}/gsd-review-cursor.md ]; then
|
||||
echo "Cursor review failed or returned empty output. stderr:" > {run_dir}/gsd-review-cursor.md
|
||||
cat {run_dir}/gsd-review-cursor.err >> {run_dir}/gsd-review-cursor.md
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: antigravity -->
|
||||
**Antigravity CLI:**
|
||||
|
||||
**Maintainer note — why this block has three layers (last updated against agy 1.0.16):**
|
||||
|
||||
`agy -p` (the `--print` non-interactive flag) works correctly on macOS and Linux: it sends the
|
||||
prompt, receives the model response, and writes it to stdout. On **native Windows** it silently
|
||||
produces no stdout output despite the API call succeeding — a bug in `text_drip.go`'s non-TTY
|
||||
flush path, tracked at https://github.com/google-antigravity/antigravity-cli/issues/27466 and
|
||||
still open as of agy 1.0.2.
|
||||
|
||||
Regardless of platform, `agy` always persists the full exchange to a transcript file on disk.
|
||||
The transcript fallback (Step 2 below) reads that file directly, giving Windows users full review
|
||||
coverage without any extra tooling. This pattern was first documented by the community MCP bridge
|
||||
at https://github.com/SinanTufekci/Claude-Code-Antigravity-CLI-MCP-Server — we inline the same
|
||||
logic here in pure bash/jq so no additional dependency is required.
|
||||
|
||||
**Stale-response guard (why the pre-flight watermark matters):**
|
||||
Without a watermark, the fallback would read the last `PLANNER_RESPONSE` entry in the transcript
|
||||
regardless of when it was written — including entries from a previous invocation in the same
|
||||
workspace. To prevent that, we record the transcript's line count *before* calling `agy -p`. In
|
||||
the fallback, we only read lines appended after that count. If no new lines were written (agy
|
||||
failed before producing a response), `_AGY_RESULT` is empty and Step 3 fires — never stale. If
|
||||
the conv-id changed (agy started a fresh session), all lines in the new file are new and we use
|
||||
skip=0.
|
||||
|
||||
**If the upstream stdout bug is fixed** (check the issue above): Step 2 silently becomes
|
||||
unreachable; stdout is non-empty and Step 1 handles it. No code change needed.
|
||||
|
||||
**If the transcript paths change** in a future `agy` release: Step 2 silently becomes a no-op
|
||||
and Step 3 fires with a clear error message in REVIEWS.md. No silent corruption. To debug:
|
||||
- `~/.gemini/antigravity-cli/cache/last_conversations.json` — workspace → conv-id map
|
||||
- `~/.gemini/antigravity-cli/brain/<id>/.system_generated/logs/transcript.jsonl`
|
||||
Filter: `source=="MODEL"`, `status=="DONE"`, `type=="PLANNER_RESPONSE"`, take the last match's `content` field.
|
||||
|
||||
Invocation specifics (verified agy 1.0.0, macOS arm64 and Linux amd64):
|
||||
- `-p` takes the prompt as a **flag value** — `echo X | agy -p` errors with "flag needs an argument: -p"
|
||||
- `--print-timeout` defaults to 5m, aligning with this workflow's global timeout
|
||||
- `--model "<name>"` selects the model (available since agy ~1.0.3; `agy models` lists
|
||||
them). When `review.models.agy` is set it is passed as `--model`; otherwise agy uses
|
||||
its persisted default (`agy models`).
|
||||
|
||||
```bash
|
||||
# Pre-flight: snapshot the transcript watermark before invoking agy.
|
||||
# Must run BEFORE agy -p — this is what prevents the fallback from reading a stale prior response.
|
||||
_AGY_WS=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
_AGY_CACHE="$HOME/.gemini/antigravity-cli/cache/last_conversations.json"
|
||||
_AGY_MARK_CONV=""
|
||||
_AGY_MARK_LINES=0
|
||||
if [ -f "$_AGY_CACHE" ]; then
|
||||
_AGY_MARK_CONV=$(jq -r --arg ws "$_AGY_WS" '
|
||||
.[$ws] //
|
||||
(to_entries
|
||||
| map(select(.key | ascii_downcase == ($ws | ascii_downcase)))
|
||||
| first | .value) //
|
||||
empty
|
||||
' "$_AGY_CACHE" 2>/dev/null)
|
||||
if [ -n "$_AGY_MARK_CONV" ] && [ "$_AGY_MARK_CONV" != "null" ]; then
|
||||
_AGY_MARK_TX="$HOME/.gemini/antigravity-cli/brain/${_AGY_MARK_CONV}/.system_generated/logs/transcript.jsonl"
|
||||
[ -f "$_AGY_MARK_TX" ] && _AGY_MARK_LINES=$(wc -l < "$_AGY_MARK_TX" | tr -d ' ')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 1 — primary invocation: stdout works on macOS, Linux, and WSL.
|
||||
# Three hardening invariants (#2073), all mirroring the Cursor block's discipline:
|
||||
# * FILE-REFERENCE prompt (not inline `$(cat …)`) — a large review prompt (≈197 KB
|
||||
# for 6 plans + CONTEXT + RESEARCH + REQUIREMENTS) overflows the exec arg list
|
||||
# (`bash: agy: Argument list too long`, rc 126), indistinguishable from a model
|
||||
# failure when stderr is suppressed.
|
||||
# * EXTERNAL `timeout` wrapper when available (GNU `timeout` / `gtimeout`) —
|
||||
# `--print-timeout` is agy's native cap but it CANNOT fire before agy creates a
|
||||
# session; under concurrent heavy runs one process can stall pre-session (no
|
||||
# `brain/<conv-id>/` dir, alive at 583 s despite `--print-timeout 300s`). The
|
||||
# external cap bounds wall-clock regardless. Stock macOS lacks `timeout`, so
|
||||
# the block probes for it and falls back to --print-timeout alone there.
|
||||
# * `--model` from `review.models.agy` when set — escape hatch for a pinned model
|
||||
# that 404s server-side (exits 0 with empty stdout AND empty transcript).
|
||||
# * stdin tied to /dev/null so agy never blocks on a tty.
|
||||
# A non-zero exit (external timeout = 124, crash, etc.) discards any partial output
|
||||
# so the Step 2 transcript fallback / Step 3 diagnostic take over.
|
||||
if [ -n "$AGY_MODEL" ] && [ "$AGY_MODEL" != "null" ]; then
|
||||
set -- --model "$AGY_MODEL"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
# #2176: grant the reviewer the repo under review. Without --add-dir, agy's
|
||||
# permission context never receives the cwd repo — the agent anchors on its own
|
||||
# ~/.gemini/antigravity-cli/scratch dir and reviews the plan text in isolation
|
||||
# (the exact failure the Review Instructions forbid). Capability-probed so an
|
||||
# older agy without --add-dir still runs; the prompt anchor below keeps
|
||||
# absolute-path reads possible on that fallback.
|
||||
if agy --help 2>/dev/null | grep -q -- '--add-dir'; then
|
||||
set -- "$@" --add-dir "$_AGY_WS"
|
||||
fi
|
||||
# #2176: anchor the prompt to the absolute repo root so repo-relative references
|
||||
# in the assembled review prompt resolve even on the no---add-dir fallback, and
|
||||
# require an explicit self-report if the reviewer still cannot read the repo.
|
||||
_AGY_PROMPT="Read the file at {run_dir}/gsd-review-prompt.md in full and carry out the review request it contains. The repository under review is at $_AGY_WS — resolve every relative file path in the review request against that absolute root and verify claims against those files. If you cannot read files under $_AGY_WS, begin your output with the exact line REVIEWED-WITHOUT-REPO-ACCESS before the review. Output only the resulting markdown review. Do not edit any files."
|
||||
# Capability-probe an external wall-clock killer (GNU coreutils `timeout` or the
|
||||
# macOS Homebrew `gtimeout`). Stock macOS ships NEITHER — a bare `timeout …` would
|
||||
# fail with rc 127 ("command not found") and silently lose the reviewer, so fall
|
||||
# back to agy's native --print-timeout alone in that case. The external cap, when
|
||||
# available, is set HIGHER than --print-timeout so it only backstops a pre-session
|
||||
# stall (which --print-timeout cannot bound — #2073 mode 3) and never pre-empts a
|
||||
# healthy run. Mirrors the probe in scripts/base64-scan.sh.
|
||||
_AGY_KILLER="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
|
||||
if [ -n "$_AGY_KILLER" ]; then
|
||||
"$_AGY_KILLER" 600 agy --print-timeout 540s "$@" -p "$_AGY_PROMPT" </dev/null 2>/dev/null > {run_dir}/gsd-review-antigravity.md
|
||||
else
|
||||
agy --print-timeout 540s "$@" -p "$_AGY_PROMPT" </dev/null 2>/dev/null > {run_dir}/gsd-review-antigravity.md
|
||||
fi
|
||||
_AGY_RC=$?
|
||||
if [ "$_AGY_RC" -ne 0 ]; then
|
||||
: > {run_dir}/gsd-review-antigravity.md
|
||||
fi
|
||||
|
||||
# Step 2 — transcript fallback: catches Windows agy -p stdout bug (and any future stdout-silent edge cases).
|
||||
# Reads only lines appended AFTER the pre-flight watermark. If agy failed before writing a new response,
|
||||
# _AGY_RESULT is empty and Step 3 fires — no stale content can leak through.
|
||||
# Undocumented paths, verified agy 1.0.0–1.0.2. See maintainer note above if these break.
|
||||
if [ ! -s {run_dir}/gsd-review-antigravity.md ]; then
|
||||
if [ -f "$_AGY_CACHE" ]; then
|
||||
_AGY_CONV=$(jq -r --arg ws "$_AGY_WS" '
|
||||
.[$ws] //
|
||||
(to_entries
|
||||
| map(select(.key | ascii_downcase == ($ws | ascii_downcase)))
|
||||
| first | .value) //
|
||||
empty
|
||||
' "$_AGY_CACHE" 2>/dev/null)
|
||||
if [ -n "$_AGY_CONV" ] && [ "$_AGY_CONV" != "null" ]; then
|
||||
_AGY_TX="$HOME/.gemini/antigravity-cli/brain/${_AGY_CONV}/.system_generated/logs/transcript.jsonl"
|
||||
if [ -f "$_AGY_TX" ]; then
|
||||
# If conv-id changed, agy started a new session — all lines are new, skip 0.
|
||||
# If same conv-id, only read lines beyond the watermark.
|
||||
[ "$_AGY_CONV" = "$_AGY_MARK_CONV" ] && _AGY_SKIP=$_AGY_MARK_LINES || _AGY_SKIP=0
|
||||
_AGY_RESULT=$(tail -n +"$((_AGY_SKIP + 1))" "$_AGY_TX" 2>/dev/null | \
|
||||
jq -r 'select(.source=="MODEL" and .status=="DONE" and .type=="PLANNER_RESPONSE") | .content' \
|
||||
2>/dev/null | tail -1)
|
||||
[ -n "$_AGY_RESULT" ] && echo "$_AGY_RESULT" > {run_dir}/gsd-review-antigravity.md
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 3 — final guard: both approaches yielded nothing (auth error, first-run setup,
|
||||
# path schema changed, 404'd pinned model, pre-session stall, etc.)
|
||||
if [ ! -s {run_dir}/gsd-review-antigravity.md ]; then
|
||||
{
|
||||
echo "Antigravity review failed or returned empty output."
|
||||
# #2073 mode 2: a pinned model that 404s exits 0 with empty stdout AND an empty
|
||||
# transcript — the only evidence is in agy's own log. Surface it instead of a
|
||||
# bare generic stub so the failure is diagnosable.
|
||||
_AGY_LOG="$HOME/.gemini/antigravity-cli/cli.log"
|
||||
if [ -f "$_AGY_LOG" ]; then
|
||||
_AGY_ERR=$(grep -iE 'agent executor error|NOT_FOUND|Publisher model' "$_AGY_LOG" | tail -3)
|
||||
if [ -n "$_AGY_ERR" ]; then
|
||||
echo "agy log hint (pinned model may be unavailable — run 'agy models' and set review.models.agy):"
|
||||
echo "$_AGY_ERR"
|
||||
fi
|
||||
fi
|
||||
# #2073 mode 3: pre-session stall tell — no new conversation dir appeared.
|
||||
echo "If no agy run started, that is the pre-session-stall case: check whether a new ~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch."
|
||||
} > {run_dir}/gsd-review-antigravity.md
|
||||
fi
|
||||
|
||||
# #2176: blind-review marker. Two tells that the reviewer ran without repo
|
||||
# access: the prompt's mandated REVIEWED-WITHOUT-REPO-ACCESS self-report in the
|
||||
# first lines of output, or the agent DECLARING the scratch dir as its
|
||||
# workspace. Both patterns are anchored — the self-report to the head of the
|
||||
# file, the scratch tell to a workspace-declaration phrasing — so a grounded
|
||||
# review that merely QUOTES these strings (e.g. reviewing this very file) is
|
||||
# never mis-stamped. Stamp a machine-readable marker so the Consensus Summary
|
||||
# down-weights the review instead of counting an ungrounded verdict at full
|
||||
# weight. (Temp file + mv, no in-place sed — BSD/GNU safe.)
|
||||
if [ -s {run_dir}/gsd-review-antigravity.md ] && \
|
||||
{ head -5 {run_dir}/gsd-review-antigravity.md | grep -q 'REVIEWED-WITHOUT-REPO-ACCESS' || \
|
||||
grep -qiE '(workspace|working) (directory|dir).{0,40}antigravity-cli/scratch' {run_dir}/gsd-review-antigravity.md; }; then
|
||||
{
|
||||
echo "> [reviewed-without-repo-access] This reviewer ran without visibility into the repo under review — down-weight its verdict in the Consensus Summary."
|
||||
echo ""
|
||||
cat {run_dir}/gsd-review-antigravity.md
|
||||
} > {run_dir}/gsd-review-antigravity.md.tmp && \
|
||||
mv {run_dir}/gsd-review-antigravity.md.tmp {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.
|
||||
|
||||
```bash
|
||||
# Shared helper: apply prompt-budget trimming for local reviewers
|
||||
# Shared budget-trim helper. Was defined inside the Ollama leg; it is lane-agnostic, so it is
|
||||
# hoisted here now that any lane may declare a promptBudgetKey. Returns non-zero when the budget
|
||||
# is too small for the minimum review set (prompt-budget exit 2 / 11).
|
||||
prepare_trimmed_prompt_for_reviewer() {
|
||||
REVIEWER_KEY="$1"
|
||||
REVIEWER_BUDGET="$2"
|
||||
OUTPUT_PROMPT="$3"
|
||||
OUTPUT_META="$4"
|
||||
|
||||
[ -z "$REVIEWER_BUDGET" ] && return 0
|
||||
[ "$REVIEWER_BUDGET" = "null" ] && return 0
|
||||
[ "$REVIEWER_BUDGET" = "0" ] && return 0
|
||||
REVIEWER_KEY="$1"; REVIEWER_BUDGET="$2"; OUTPUT_PROMPT="$3"; OUTPUT_META="$4"
|
||||
|
||||
PLAN_FILE_ARGS=""
|
||||
for p in {run_dir}/gsd-review-plan-*.md; do
|
||||
for p in "$RUN_DIR"/gsd-review-plan-*.md; do
|
||||
[ -f "$p" ] && PLAN_FILE_ARGS="$PLAN_FILE_ARGS --plan-file $p"
|
||||
done
|
||||
PROJECT_ARG=""
|
||||
[ -f "{run_dir}/gsd-review-project.md" ] && PROJECT_ARG="--project-file {run_dir}/gsd-review-project.md"
|
||||
[ -f "$RUN_DIR/gsd-review-project.md" ] && PROJECT_ARG="--project-file $RUN_DIR/gsd-review-project.md"
|
||||
CONTEXT_ARG=""
|
||||
[ -f "{run_dir}/gsd-review-context.md" ] && CONTEXT_ARG="--context-file {run_dir}/gsd-review-context.md"
|
||||
[ -f "$RUN_DIR/gsd-review-context.md" ] && CONTEXT_ARG="--context-file $RUN_DIR/gsd-review-context.md"
|
||||
RESEARCH_ARG=""
|
||||
[ -f "{run_dir}/gsd-review-research.md" ] && RESEARCH_ARG="--research-file {run_dir}/gsd-review-research.md"
|
||||
[ -f "$RUN_DIR/gsd-review-research.md" ] && RESEARCH_ARG="--research-file $RUN_DIR/gsd-review-research.md"
|
||||
REQUIREMENTS_ARG=""
|
||||
[ -f "{run_dir}/gsd-review-requirements.md" ] && REQUIREMENTS_ARG="--requirements-file {run_dir}/gsd-review-requirements.md"
|
||||
[ -f "$RUN_DIR/gsd-review-requirements.md" ] && REQUIREMENTS_ARG="--requirements-file $RUN_DIR/gsd-review-requirements.md"
|
||||
|
||||
gsd_run query prompt-budget \
|
||||
--budget "$REVIEWER_BUDGET" \
|
||||
--instructions-file "{run_dir}/gsd-review-instructions.md" \
|
||||
--roadmap-file "{run_dir}/gsd-review-roadmap.md" \
|
||||
--instructions-file "$RUN_DIR/gsd-review-instructions.md" \
|
||||
--roadmap-file "$RUN_DIR/gsd-review-roadmap.md" \
|
||||
$PLAN_FILE_ARGS $PROJECT_ARG $CONTEXT_ARG $RESEARCH_ARG $REQUIREMENTS_ARG \
|
||||
--output-prompt "$OUTPUT_PROMPT" \
|
||||
--output-metadata "$OUTPUT_META"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Resolve prompt budget for Ollama: per-reviewer override > global default > null
|
||||
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.ollama --raw 2>/dev/null || echo "null")
|
||||
# #2797: -1 = unset; 0 legitimately means "do not trim".
|
||||
if [ -z "$OLLAMA_REVIEWER_BUDGET" ] || [ "$OLLAMA_REVIEWER_BUDGET" = "null" ] || [ "$OLLAMA_REVIEWER_BUDGET" = "-1" ]; then
|
||||
OLLAMA_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
|
||||
fi
|
||||
gsd_run query review-lane plan \
|
||||
--selected "$SELECTED_REVIEWERS" --run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" --json \
|
||||
> "$RUN_DIR/gsd-review-lanes.json"
|
||||
|
||||
# Apply budget trim for Ollama if a budget is configured
|
||||
OLLAMA_PROMPT_FILE="{run_dir}/gsd-review-prompt.md"
|
||||
OLLAMA_SKIP=0
|
||||
if [ -n "$OLLAMA_REVIEWER_BUDGET" ] && [ "$OLLAMA_REVIEWER_BUDGET" != "null" ] && [ "$OLLAMA_REVIEWER_BUDGET" != "0" ]; then
|
||||
OLLAMA_TRIMMED_PROMPT="{run_dir}/gsd-review-prompt-ollama.md"
|
||||
OLLAMA_TRIM_META="{run_dir}/gsd-review-prompt-ollama.metadata.json"
|
||||
prepare_trimmed_prompt_for_reviewer "ollama" "$OLLAMA_REVIEWER_BUDGET" "$OLLAMA_TRIMMED_PROMPT" "$OLLAMA_TRIM_META"
|
||||
OLLAMA_EXIT=$?
|
||||
if [ $OLLAMA_EXIT -ne 0 ]; then
|
||||
if [ $OLLAMA_EXIT -eq 2 ] || [ $OLLAMA_EXIT -eq 11 ]; then
|
||||
echo "WARNING: prompt budget for ollama (${OLLAMA_REVIEWER_BUDGET} tokens) is too small for the minimum review set. Skipping Ollama reviewer." >&2
|
||||
for SLUG in $(echo "$SELECTED_REVIEWERS" | tr ',' ' '); do
|
||||
# Per-lane prompt budget. The lane declares its own `promptBudgetKey`; `plan` resolved it,
|
||||
# applying #2797's sentinel rule (-1 = unset → fall back to the global budget; 0 legitimately
|
||||
# means "do not trim this lane"). Trimming itself stays in prompt-budget, which owns it.
|
||||
LANE_BUDGET=$(gsd_run query review-lane plan --selected "$SLUG" --run-dir "$RUN_DIR" \
|
||||
--repo-root "$REPO_ROOT" --json 2>/dev/null \
|
||||
| sed -n 's/.*"promptBudget": *\([0-9-]*\).*/\1/p' | head -1)
|
||||
PROMPT_ARG=""
|
||||
if [ -n "$LANE_BUDGET" ] && [ "$LANE_BUDGET" != "null" ] && [ "$LANE_BUDGET" -gt 0 ] 2>/dev/null; then
|
||||
TRIMMED="$RUN_DIR/gsd-review-prompt-$SLUG.md"
|
||||
if prepare_trimmed_prompt_for_reviewer "$SLUG" "$LANE_BUDGET" "$TRIMMED" \
|
||||
"$RUN_DIR/gsd-review-prompt-$SLUG.metadata.json"; then
|
||||
PROMPT_ARG="--prompt-file $TRIMMED"
|
||||
else
|
||||
echo "WARNING: prompt-budget returned unexpected exit code ${OLLAMA_EXIT} for ollama. Skipping Ollama reviewer." >&2
|
||||
# A budget too small for the minimum review set drops the lane just as silently as an empty
|
||||
# response used to (#2605), so leave the skip visible in the review output, not only on stderr.
|
||||
echo "$SLUG review skipped: prompt budget (${LANE_BUDGET} tokens) too small for the minimum review set." \
|
||||
> "$RUN_DIR/gsd-review-$SLUG.md"
|
||||
continue
|
||||
fi
|
||||
OLLAMA_SKIP=1
|
||||
else
|
||||
OLLAMA_PROMPT_FILE="$OLLAMA_TRIMMED_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$OLLAMA_SKIP" != "1" ]; then
|
||||
OLLAMA_HOST=$(gsd_run query config-get review.ollama_host --raw 2>/dev/null || echo "")
|
||||
if [ -z "$OLLAMA_HOST" ] || [ "$OLLAMA_HOST" = "null" ]; then OLLAMA_HOST="http://localhost:11434"; fi
|
||||
OLLAMA_MODEL=$(gsd_run query config-get review.models.ollama --raw 2>/dev/null || echo "")
|
||||
if [ -z "$OLLAMA_MODEL" ] || [ "$OLLAMA_MODEL" = "null" ]; then
|
||||
OLLAMA_MODEL=$(curl -s --max-time 2 "${OLLAMA_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "llama3"' 2>/dev/null || echo "llama3")
|
||||
fi
|
||||
# #2605: brought to parity with the LM Studio / llama.cpp legs below. Ollama
|
||||
# already emitted a non-empty stub, so it never silently vanished — but it was
|
||||
# the LEAST diagnosable leg: bare `-s` (which suppresses curl's error text as
|
||||
# well as the progress meter), stderr to /dev/null, and the response piped
|
||||
# straight into jq so the body — where an OpenAI-compatible server puts its error
|
||||
# JSON on an HTTP 4xx/5xx, with curl still exiting 0 — was discarded unread.
|
||||
OLLAMA_RESPONSE=$(jq -n --rawfile content "$OLLAMA_PROMPT_FILE" \
|
||||
--arg model "$OLLAMA_MODEL" \
|
||||
'{model: $model, messages: [{role: "user", content: $content}]}' | \
|
||||
curl -sS --max-time 120 -X POST "${OLLAMA_HOST}/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" -d @- 2>{run_dir}/gsd-review-ollama.err)
|
||||
OLLAMA_CONTENT=$(echo "$OLLAMA_RESPONSE" | jq -r '.choices[0].message.content // ""' 2>/dev/null || echo "")
|
||||
case "$OLLAMA_CONTENT" in
|
||||
*[![:space:]]*) : ;;
|
||||
*) OLLAMA_CONTENT="" ;;
|
||||
esac
|
||||
if [ -n "$OLLAMA_CONTENT" ]; then
|
||||
printf '%s\n' "$OLLAMA_CONTENT" > {run_dir}/gsd-review-ollama.md
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-ollama.md ]; then
|
||||
echo "Warning: Ollama returned empty content — see {run_dir}/gsd-review-ollama.md" >&2
|
||||
echo "Ollama review failed or returned empty output. stderr:" > {run_dir}/gsd-review-ollama.md
|
||||
cat {run_dir}/gsd-review-ollama.err >> {run_dir}/gsd-review-ollama.md 2>/dev/null
|
||||
echo "Raw response body:" >> {run_dir}/gsd-review-ollama.md
|
||||
printf '%s\n' "$OLLAMA_RESPONSE" >> {run_dir}/gsd-review-ollama.md
|
||||
fi
|
||||
else
|
||||
echo "Ollama review skipped: prompt budget (${OLLAMA_REVIEWER_BUDGET} tokens) too small for the minimum review set." > {run_dir}/gsd-review-ollama.md
|
||||
fi
|
||||
# One invocation, whatever the lane's transport, prompt channel, output channel or handler.
|
||||
# `--explicit` marks a lane the user NAMED: ADR-2782 D4 — not finding a lane nobody asked for is
|
||||
# normal, failing to run one somebody asked for is an error.
|
||||
gsd_run query review-lane invoke --slug "$SLUG" \
|
||||
--run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" $PROMPT_ARG $EXPLICIT_FLAG --json \
|
||||
>> "$RUN_DIR/gsd-review-lane-results.jsonl"
|
||||
done
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: lm_studio -->
|
||||
**LM Studio (local, OpenAI-compatible):**
|
||||
```bash
|
||||
# Resolve prompt budget for LM Studio: per-reviewer override > global default > null
|
||||
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.lm_studio --raw 2>/dev/null || echo "null")
|
||||
# #2797: -1 = unset; 0 legitimately means "do not trim".
|
||||
if [ -z "$LM_STUDIO_REVIEWER_BUDGET" ] || [ "$LM_STUDIO_REVIEWER_BUDGET" = "null" ] || [ "$LM_STUDIO_REVIEWER_BUDGET" = "-1" ]; then
|
||||
LM_STUDIO_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
|
||||
fi
|
||||
Each lane leaves `{run_dir}/gsd-review-<slug>.md` — its review, or a diagnostic stub carrying the
|
||||
captured stderr (and, for an OpenAI-compatible lane, the raw response body, where such a server puts
|
||||
its error JSON on an HTTP 4xx/5xx while still exiting 0). A stub is never mistaken for a clean
|
||||
review: it keeps its "failed or returned empty output" header (#2494/#2605/#2794).
|
||||
|
||||
# Apply budget trim for LM Studio if a budget is configured
|
||||
LM_STUDIO_PROMPT_FILE="{run_dir}/gsd-review-prompt.md"
|
||||
LM_STUDIO_SKIP=0
|
||||
if [ -n "$LM_STUDIO_REVIEWER_BUDGET" ] && [ "$LM_STUDIO_REVIEWER_BUDGET" != "null" ] && [ "$LM_STUDIO_REVIEWER_BUDGET" != "0" ]; then
|
||||
LM_STUDIO_TRIMMED_PROMPT="{run_dir}/gsd-review-prompt-lm_studio.md"
|
||||
LM_STUDIO_TRIM_META="{run_dir}/gsd-review-prompt-lm_studio.metadata.json"
|
||||
prepare_trimmed_prompt_for_reviewer "lm_studio" "$LM_STUDIO_REVIEWER_BUDGET" "$LM_STUDIO_TRIMMED_PROMPT" "$LM_STUDIO_TRIM_META"
|
||||
LM_STUDIO_EXIT=$?
|
||||
if [ $LM_STUDIO_EXIT -ne 0 ]; then
|
||||
if [ $LM_STUDIO_EXIT -eq 2 ] || [ $LM_STUDIO_EXIT -eq 11 ]; then
|
||||
echo "WARNING: prompt budget for lm_studio (${LM_STUDIO_REVIEWER_BUDGET} tokens) is too small for the minimum review set. Skipping LM Studio reviewer." >&2
|
||||
else
|
||||
echo "WARNING: prompt-budget returned unexpected exit code ${LM_STUDIO_EXIT} for lm_studio. Skipping LM Studio reviewer." >&2
|
||||
fi
|
||||
LM_STUDIO_SKIP=1
|
||||
else
|
||||
LM_STUDIO_PROMPT_FILE="$LM_STUDIO_TRIMMED_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$LM_STUDIO_SKIP" != "1" ]; then
|
||||
LM_STUDIO_HOST=$(gsd_run query config-get review.lm_studio_host --raw 2>/dev/null || echo "")
|
||||
if [ -z "$LM_STUDIO_HOST" ] || [ "$LM_STUDIO_HOST" = "null" ]; then LM_STUDIO_HOST="http://localhost:1234"; fi
|
||||
LM_STUDIO_MODEL=$(gsd_run query config-get review.models.lm_studio --raw 2>/dev/null || echo "")
|
||||
if [ -z "$LM_STUDIO_MODEL" ] || [ "$LM_STUDIO_MODEL" = "null" ]; then
|
||||
LM_STUDIO_MODEL=$(curl -s --max-time 2 "${LM_STUDIO_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "local-model"' 2>/dev/null || echo "local-model")
|
||||
fi
|
||||
# #2605: same guard as the claude/gemini/codex legs above (#2494/#2592). Two
|
||||
# changes make a dropped lane diagnosable rather than silently omitted:
|
||||
# 1. `-sS` instead of `-s`. Plain `-s` silences curl's ERROR text too, so an
|
||||
# unreachable endpoint produced no message anywhere. `-S` restores errors
|
||||
# while keeping the progress meter off; they land in the .err sidecar.
|
||||
# 2. An `[ ! -s … ]` stub. Previously nothing was written when content was
|
||||
# empty, so the file never existed, write_reviews omitted the section, and
|
||||
# the result was indistinguishable from the reviewer never being selected.
|
||||
# The raw response body is appended too: an HTTP 4xx/5xx from an OpenAI-compatible
|
||||
# server exits 0 with the error JSON in the BODY, so stderr alone would be empty.
|
||||
LM_STUDIO_RESPONSE=$(jq -n --rawfile content "$LM_STUDIO_PROMPT_FILE" \
|
||||
--arg model "$LM_STUDIO_MODEL" \
|
||||
'{model: $model, messages: [{role: "user", content: $content}]}' | \
|
||||
curl -sS --max-time 120 -X POST "${LM_STUDIO_HOST}/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" -d @- 2>{run_dir}/gsd-review-lm_studio.err)
|
||||
LM_STUDIO_ACTUAL_MODEL=$(echo "$LM_STUDIO_RESPONSE" | jq -r '.model // ""' 2>/dev/null || echo "")
|
||||
if [ -n "$LM_STUDIO_ACTUAL_MODEL" ] && [ "$LM_STUDIO_ACTUAL_MODEL" != "null" ] && [ "$LM_STUDIO_ACTUAL_MODEL" != "$LM_STUDIO_MODEL" ]; then
|
||||
echo "Warning: LM Studio served model '$LM_STUDIO_ACTUAL_MODEL' but '$LM_STUDIO_MODEL' was requested. Review may be from a different model." >&2
|
||||
fi
|
||||
LM_STUDIO_CONTENT=$(echo "$LM_STUDIO_RESPONSE" | jq -r '.choices[0].message.content // ""' 2>/dev/null || echo "")
|
||||
# A whitespace-only reply must count as empty. `[ ! -s … ]` counts BYTES, so a
|
||||
# response of " " would be written out and pass the guard as a "successful"
|
||||
# but vacuous review — the same indistinguishable-from-success outcome the guard
|
||||
# exists to prevent. Command substitution strips trailing newlines but not
|
||||
# spaces, so this case-glob is what actually closes it.
|
||||
case "$LM_STUDIO_CONTENT" in
|
||||
*[![:space:]]*) : ;;
|
||||
*) LM_STUDIO_CONTENT="" ;;
|
||||
esac
|
||||
# printf, not echo: `echo "$VAR"` swallows a value that is exactly `-n`/`-e`/`-E`
|
||||
# and would write 0 bytes, misclassifying a real reply as empty. Same idiom the
|
||||
# OpenCode leg already uses above.
|
||||
if [ -n "$LM_STUDIO_CONTENT" ]; then
|
||||
printf '%s\n' "$LM_STUDIO_CONTENT" > {run_dir}/gsd-review-lm_studio.md
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-lm_studio.md ]; then
|
||||
echo "Warning: LM Studio returned empty content — see {run_dir}/gsd-review-lm_studio.md" >&2
|
||||
echo "LM Studio review failed or returned empty output. stderr:" > {run_dir}/gsd-review-lm_studio.md
|
||||
cat {run_dir}/gsd-review-lm_studio.err >> {run_dir}/gsd-review-lm_studio.md 2>/dev/null
|
||||
echo "Raw response body:" >> {run_dir}/gsd-review-lm_studio.md
|
||||
printf '%s\n' "$LM_STUDIO_RESPONSE" >> {run_dir}/gsd-review-lm_studio.md
|
||||
fi
|
||||
else
|
||||
# A budget skip drops the lane just as silently as an empty response did: no
|
||||
# file, so write_reviews omits the section entirely. Leave the same diagnosable
|
||||
# stub so the skip is visible in the review output, not only on stderr (#2605).
|
||||
echo "LM Studio review skipped: prompt budget (${LM_STUDIO_REVIEWER_BUDGET} tokens) too small for the minimum review set." > {run_dir}/gsd-review-lm_studio.md
|
||||
fi
|
||||
```
|
||||
|
||||
<!-- reviewer-lane: llama_cpp -->
|
||||
**llama.cpp (local, OpenAI-compatible):**
|
||||
```bash
|
||||
# Resolve prompt budget for llama.cpp: per-reviewer override > global default > null
|
||||
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens_per_reviewer.llama_cpp --raw 2>/dev/null || echo "null")
|
||||
# #2797: -1 = unset; 0 legitimately means "do not trim".
|
||||
if [ -z "$LLAMA_CPP_REVIEWER_BUDGET" ] || [ "$LLAMA_CPP_REVIEWER_BUDGET" = "null" ] || [ "$LLAMA_CPP_REVIEWER_BUDGET" = "-1" ]; then
|
||||
LLAMA_CPP_REVIEWER_BUDGET=$(gsd_run query config-get review.max_prompt_tokens --raw 2>/dev/null || echo "null")
|
||||
fi
|
||||
|
||||
# Apply budget trim for llama.cpp if a budget is configured
|
||||
LLAMA_CPP_PROMPT_FILE="{run_dir}/gsd-review-prompt.md"
|
||||
LLAMA_CPP_SKIP=0
|
||||
if [ -n "$LLAMA_CPP_REVIEWER_BUDGET" ] && [ "$LLAMA_CPP_REVIEWER_BUDGET" != "null" ] && [ "$LLAMA_CPP_REVIEWER_BUDGET" != "0" ]; then
|
||||
LLAMA_CPP_TRIMMED_PROMPT="{run_dir}/gsd-review-prompt-llama_cpp.md"
|
||||
LLAMA_CPP_TRIM_META="{run_dir}/gsd-review-prompt-llama_cpp.metadata.json"
|
||||
prepare_trimmed_prompt_for_reviewer "llama_cpp" "$LLAMA_CPP_REVIEWER_BUDGET" "$LLAMA_CPP_TRIMMED_PROMPT" "$LLAMA_CPP_TRIM_META"
|
||||
LLAMA_CPP_EXIT=$?
|
||||
if [ $LLAMA_CPP_EXIT -ne 0 ]; then
|
||||
if [ $LLAMA_CPP_EXIT -eq 2 ] || [ $LLAMA_CPP_EXIT -eq 11 ]; then
|
||||
echo "WARNING: prompt budget for llama_cpp (${LLAMA_CPP_REVIEWER_BUDGET} tokens) is too small for the minimum review set. Skipping llama.cpp reviewer." >&2
|
||||
else
|
||||
echo "WARNING: prompt-budget returned unexpected exit code ${LLAMA_CPP_EXIT} for llama_cpp. Skipping llama.cpp reviewer." >&2
|
||||
fi
|
||||
LLAMA_CPP_SKIP=1
|
||||
else
|
||||
LLAMA_CPP_PROMPT_FILE="$LLAMA_CPP_TRIMMED_PROMPT"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$LLAMA_CPP_SKIP" != "1" ]; then
|
||||
LLAMA_CPP_HOST=$(gsd_run query config-get review.llama_cpp_host --raw 2>/dev/null || echo "")
|
||||
if [ -z "$LLAMA_CPP_HOST" ] || [ "$LLAMA_CPP_HOST" = "null" ]; then LLAMA_CPP_HOST="http://localhost:8080"; fi
|
||||
LLAMA_CPP_MODEL=$(gsd_run query config-get review.models.llama_cpp --raw 2>/dev/null || echo "")
|
||||
if [ -z "$LLAMA_CPP_MODEL" ] || [ "$LLAMA_CPP_MODEL" = "null" ]; then
|
||||
LLAMA_CPP_MODEL=$(curl -s --max-time 2 "${LLAMA_CPP_HOST}/v1/models" 2>/dev/null | jq -r '.data[0].id // "local-model"' 2>/dev/null || echo "local-model")
|
||||
fi
|
||||
# #2605: same guard as the LM Studio leg above. The response is captured to a
|
||||
# variable FIRST rather than piped straight into jq — piping discarded the raw
|
||||
# body, which is exactly where an OpenAI-compatible server puts its error JSON on
|
||||
# an HTTP 4xx/5xx (curl still exits 0), leaving nothing to diagnose.
|
||||
LLAMA_CPP_RESPONSE=$(jq -n --rawfile content "$LLAMA_CPP_PROMPT_FILE" \
|
||||
--arg model "$LLAMA_CPP_MODEL" \
|
||||
'{model: $model, messages: [{role: "user", content: $content}]}' | \
|
||||
curl -sS --max-time 120 -X POST "${LLAMA_CPP_HOST}/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" -d @- 2>{run_dir}/gsd-review-llama_cpp.err)
|
||||
LLAMA_CPP_CONTENT=$(echo "$LLAMA_CPP_RESPONSE" | jq -r '.choices[0].message.content // ""' 2>/dev/null || echo "")
|
||||
# Whitespace-only reply counts as empty; printf not echo. See the LM Studio leg
|
||||
# above for why both are required.
|
||||
case "$LLAMA_CPP_CONTENT" in
|
||||
*[![:space:]]*) : ;;
|
||||
*) LLAMA_CPP_CONTENT="" ;;
|
||||
esac
|
||||
if [ -n "$LLAMA_CPP_CONTENT" ]; then
|
||||
printf '%s\n' "$LLAMA_CPP_CONTENT" > {run_dir}/gsd-review-llama_cpp.md
|
||||
fi
|
||||
if [ ! -s {run_dir}/gsd-review-llama_cpp.md ]; then
|
||||
echo "Warning: llama.cpp returned empty content — see {run_dir}/gsd-review-llama_cpp.md" >&2
|
||||
echo "llama.cpp review failed or returned empty output. stderr:" > {run_dir}/gsd-review-llama_cpp.md
|
||||
cat {run_dir}/gsd-review-llama_cpp.err >> {run_dir}/gsd-review-llama_cpp.md 2>/dev/null
|
||||
echo "Raw response body:" >> {run_dir}/gsd-review-llama_cpp.md
|
||||
printf '%s\n' "$LLAMA_CPP_RESPONSE" >> {run_dir}/gsd-review-llama_cpp.md
|
||||
fi
|
||||
else
|
||||
echo "llama.cpp review skipped: prompt budget (${LLAMA_CPP_REVIEWER_BUDGET} tokens) too small for the minimum review set." > {run_dir}/gsd-review-llama_cpp.md
|
||||
fi
|
||||
```
|
||||
|
||||
If a CLI or local server fails, log the error and continue with remaining reviewers.
|
||||
A lane that will not run reports a typed reason rather than an empty file — `missing_binary`,
|
||||
`probe_failed`, `probe_timeout`, `missing_required_binary`, `host_unreachable`,
|
||||
`egress_host_changed`, `unknown_handler`, `budget_too_small`. **`egress_host_changed` means the lane
|
||||
was consented to send plans to one destination and `.planning/config.json` now names another; it is
|
||||
blocked, not silently redirected** (ADR-2782 D5).
|
||||
|
||||
Display progress:
|
||||
```
|
||||
@@ -969,83 +434,26 @@ trimmed_reviewers: # only present if at least one reviewer was trimmed
|
||||
|
||||
# Cross-AI Plan Review — Phase {N}
|
||||
|
||||
## Gemini Review
|
||||
<!-- Sections are RENDERED from each lane's declared `reviewsSection`, in descriptor order.
|
||||
There is deliberately no hardcoded per-reviewer heading list here any more: a hand-maintained
|
||||
list is exactly the drift #2781 was filed about, and it silently disagreed with the roster.
|
||||
`gsd_run query review-lane sections --selected "$SELECTED_REVIEWERS"` emits
|
||||
`<slug><TAB><reviewsSection>` in order; for each row, emit:
|
||||
|
||||
{gemini review content}
|
||||
## <reviewsSection> Review
|
||||
|
||||
---
|
||||
{contents of {run_dir}/gsd-review-<slug>.md}
|
||||
|
||||
## Claude Review
|
||||
---
|
||||
|
||||
{claude review content}
|
||||
Two headings must NOT be generated from this list, because they are not lanes:
|
||||
* `## <Adapter> Review (<instance>)` — an ADR-1517 reviewer INSTANCE resolves THROUGH a lane
|
||||
and is rendered from the instance list, not the lane list (ADR-2782 D8).
|
||||
* `## Consensus Summary` — not a review section at all.
|
||||
|
||||
---
|
||||
|
||||
## Codex Review
|
||||
|
||||
{codex review content}
|
||||
|
||||
---
|
||||
|
||||
## CodeRabbit Review
|
||||
|
||||
{coderabbit review content}
|
||||
|
||||
---
|
||||
|
||||
## OpenCode Review
|
||||
|
||||
{opencode review content}
|
||||
|
||||
---
|
||||
|
||||
## 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}
|
||||
|
||||
---
|
||||
|
||||
## Cursor Review
|
||||
|
||||
{cursor review content}
|
||||
|
||||
---
|
||||
|
||||
## Antigravity Review
|
||||
|
||||
{antigravity review content}
|
||||
|
||||
---
|
||||
|
||||
## Ollama Review
|
||||
|
||||
{ollama review content}
|
||||
|
||||
---
|
||||
|
||||
## LM Studio Review
|
||||
|
||||
{lm_studio review content}
|
||||
|
||||
---
|
||||
|
||||
## llama.cpp Review
|
||||
|
||||
{llama_cpp review content}
|
||||
|
||||
---
|
||||
A lane whose `evidenceClass` is `diff-only` (CodeRabbit) carries its caveat from data: it never
|
||||
received the source-grounding prompt, so its verdict is folded in as a diff observation and is
|
||||
not weighted as a grounded plan review. -->
|
||||
|
||||
## Consensus Summary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user