* 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:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user