feat(#3034): add opt-in parallel reviewer lanes (#3822)

* test(#3034): failing-first coverage for opt-in parallel reviewer lanes

Executes the real invoke_reviewers dispatch block from review.md against a
stubbed gsd_run seam rather than pattern-matching the workflow text, so the
two properties that actually carry risk are observable: that every lane is
joined before aggregation, and that concurrent lanes cannot tear a line in
gsd-review-lane-results.jsonl.

Concurrency is proven by a barrier fixture, not by elapsed time -- each stub
lane blocks until all lanes have checked in, which can only complete if they
overlap.

Red against the current sequential dispatch, by design.

Refs #3034

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

* feat(#3034): add opt-in parallel reviewer lanes

Reviewer lanes within one review pass inspect the same immutable plan
snapshot and have no dependency on one another, but were dispatched strictly
one at a time, so a multi-reviewer pass cost roughly the sum of its lanes.
The serialization is a deliberate protection against provider rate limits,
so it stays the default; review.parallel_lanes opts a project out of it.

The loop body is hoisted into run_review_lane so the sequential and
concurrent paths share one body -- two hand-synced dispatch bodies is the
divergence class ADR-2782 spent a phase deleting. Each lane writes a
slug-scoped result file, concatenated in selection order after the join:
concurrent O_APPEND is atomic only below PIPE_BUF, and write_reviews parses
that JSONL to render the models:/model_sources: frontmatter, so a torn line
is a broken REVIEWS.md rather than a cosmetic log defect. Aggregating in
selection order also keeps the artifact byte-identical between the two paths.

The guard is strict equality on "true" and falls back to sequential when
config-get fails -- the opposite polarity from the commit_docs guard,
because failing open here fires the very requests the default prevents.

Also corrects docs/COMMANDS.md and its four locale mirrors, which described
--all as running every configured reviewer in parallel when dispatch was in
fact sequential.

Closes #3034

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

* fix(#3034): de-duplicate dispatch slugs and scope lane locals

Review finding (Standards axis): a slug repeated in SELECTED_REVIEWERS would
put two concurrent background jobs on the same > -truncated per-lane result
file. The shared-append form this replaced could not corrupt itself that way,
so de-duplicating is what keeps the concurrent path no worse than the
sequential one.

Selection de-dupes today -- the roster is a Set and review.default_reviewers
normalizes lowercase-unique -- but reachability analysis is not a contract,
which is the same reason the roster derivation itself is guarded.

Splitting once into DISPATCH_SLUGS also removes the duplicated tr-split the
same review flagged: the dispatch and aggregation loops now share one list,
which is what guarantees they walk the same slugs in the same order. A plain
string accumulator rather than an array, because zsh and bash disagree on
array indexing and this block runs under both.

Also scopes run_review_lane's locals. Not a live fix -- each dispatched call
already forks its own subshell -- but it makes the isolation a property of the
function rather than of the dispatch mechanism happening to fork.

Refs #3034

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

* test(#3034): acknowledge review.md growth, drop spent 2295 ack

The differential attribution gate reported review.md growing 4173 bytes
(30712 -> 34885) with no live acknowledgment. Adds the per-PR fragment it
asks for, naming only the one path it reported.

Deleting tests/emitted-drift-acks/2295-resolved-model.json is required, not
opportunistic. That fragment declared review.md and nothing else, and its
ripple is already absorbed into the base, so it is spent -- it can no longer
clear anything, which is why the gate still reported review.md as
unacknowledged. It could not simply be left alone either: two ack sources may
never name the same path, so it blocked this PR's fragment outright.
CONTRIBUTING is explicit that a fragment whose last entry is removed gets
deleted with it, because an empty fragment signals nothing while its presence
reads as a live alarm.

Refs #3034

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

* chore(#3034): backfill changeset PR number

Refs #3034

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

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-24 15:25:22 -04:00
committed by GitHub
parent a84f756303
commit a2387a0545
14 changed files with 858 additions and 10 deletions

View File

@@ -316,7 +316,11 @@ reintroduce the flag (even spelled out in prose — a regression test bans the l
If `section_manifest` is `null` or `"reviewer-instances-note-2"` is in its `included` list: read and execute `gsd-core/workflows/review/steps/reviewer-instances-note-2.md`. Otherwise skip — do not read the file.
<!-- /gsd:section -->
Lanes run **sequentially, not in parallel** — concurrent invocation trips provider rate limits.
Lanes run **sequentially by default** — concurrent invocation trips provider rate limits, and a lane
lost to one is a cross-AI review that quietly went blind in one eye. A project whose providers can
accept the concurrency opts in with `review.parallel_lanes: true` (#3034): the selected lanes are
dispatched together and **all** joined before aggregation. The default is unchanged, and convergence
cycles stay sequential either way — only the lanes *within* one pass overlap.
```bash
# #2962: zsh aborts the block on an unmatched for-list glob (nomatch); bash passes it through. nullglob both.
@@ -327,6 +331,13 @@ 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.
# #3034: opt-in concurrent lane dispatch. STRICT equality on "true" is deliberate — "1", "yes" and
# "TRUE" must NOT opt in, so a mistyped config gets the conservative behaviour rather than firing
# concurrent requests at a rate-limited provider. Note the `|| echo "false"` fallback is the
# OPPOSITE polarity from the commit_docs guard, which fails OPEN: there, failing open preserves the
# user's intent; here it would fire exactly the requests the default exists to prevent.
PARALLEL_LANES=$(gsd_run query config-get review.parallel_lanes --raw 2>/dev/null || echo "false")
# 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).
@@ -360,7 +371,20 @@ gsd_run query review-lane plan \
--selected "$SELECTED_REVIEWERS" --run-dir "$RUN_DIR" --repo-root "$REPO_ROOT" --json \
> "$RUN_DIR/gsd-review-lanes.json"
for SLUG in $(echo "$SELECTED_REVIEWERS" | tr ',' ' '); do
# One lane, start to finish. Hoisted into a function so the sequential and concurrent paths share
# ONE body: two dispatch bodies kept in sync by hand is the generative-fix divergence ADR-2782 spent
# a phase deleting, and it is what let #2494/#2605 be filed twice as the same defect.
#
# The result goes to a SLUG-SCOPED file, never a shared append. Concurrent O_APPEND is atomic only
# below PIPE_BUF (4096 on Linux, 512 on some platforms), so a lane result above that bound could
# interleave — and write_reviews parses this JSONL to render the models:/model_sources: frontmatter,
# so a torn line is a broken REVIEWS.md, not a cosmetic log defect.
run_review_lane() {
# `local` is hygiene, not a live fix: each `&`-dispatched call already forks its own subshell, so
# concurrent lanes cannot share these today. Scoped anyway so the isolation is a property of this
# function rather than of the dispatch mechanism happening to fork.
local SLUG LANE_BUDGET PROMPT_ARG TRIMMED
SLUG="$1"
# 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.
@@ -378,7 +402,9 @@ for SLUG in $(echo "$SELECTED_REVIEWERS" | tr ',' ' '); do
# 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
# Was `continue` when this was a loop body. Inside a function that keyword is not the loop
# control it looks like — `return 0` is what skips this lane and leaves it with no result line.
return 0
fi
fi
@@ -387,7 +413,50 @@ for SLUG in $(echo "$SELECTED_REVIEWERS" | tr ',' ' '); do
# 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"
> "$RUN_DIR/gsd-review-lane-result-$SLUG.json"
}
# Split ONCE, de-duplicated, and reuse for both loops below. Two reasons, and the second is
# load-bearing: a slug repeated in SELECTED_REVIEWERS would put TWO concurrent background jobs on
# `> "$RUN_DIR/gsd-review-lane-result-$SLUG.json"` — the same file, both truncating. The shared-append
# form this replaced could not corrupt itself that way, so de-duping is what keeps the concurrent
# path no worse than the sequential one. Selection de-dupes today (the roster is a Set;
# review.default_reviewers normalizes lowercase-unique), but reachability analysis is not a contract
# and the next caller should not have to redo it.
#
# A plain string accumulator, not an array: zsh and bash disagree on array indexing and this block
# runs under both (see the nullglob/NULL_GLOB pairing above).
DISPATCH_SLUGS=""
for SLUG in $(echo "$SELECTED_REVIEWERS" | tr ',' ' '); do
case " $DISPATCH_SLUGS " in
*" $SLUG "*) continue ;;
esac
DISPATCH_SLUGS="$DISPATCH_SLUGS $SLUG"
done
for SLUG in $DISPATCH_SLUGS; do
if [ "$PARALLEL_LANES" = "true" ]; then
run_review_lane "$SLUG" &
else
run_review_lane "$SLUG"
fi
done
# Join every dispatched lane. A bare `wait` with no background jobs returns 0, so the sequential
# path needs no guard around it. NOTHING below this line may run before every lane has finished —
# write_reviews renders REVIEWS.md and the consensus summary from the aggregate below, and a review
# assembled from a partial set looks complete while silently missing a reviewer.
wait
# Aggregate in SELECTED_REVIEWERS order, NOT completion order, so the JSONL a concurrent run
# produces is byte-identical to the one a sequential run produces. This is post-join and therefore
# single-threaded, so `>>` here is safe. A lane that was budget-skipped, or that never started,
# leaves no result file and correctly contributes no line.
for SLUG in $DISPATCH_SLUGS; do
LANE_RESULT="$RUN_DIR/gsd-review-lane-result-$SLUG.json"
if [ -f "$LANE_RESULT" ]; then
cat "$LANE_RESULT" >> "$RUN_DIR/gsd-review-lane-results.jsonl"
fi
done
```