* 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>
tests/emitted-drift-acks/
Per-PR acknowledgment fragments for the differential attribution check
(tests/emitted-attribution.test.cjs, ADR-2719 / #2789 / #2914).
This directory being empty is the healthy steady state. A fragment appearing
in a diff is the alarm; a fragment sitting here on next is spent cruft.
README.md is not a fragment — every reader filters on .json — and exists so
the directory (which CONTEXT.md and CONTRIBUTING.md both reference by path)
survives the sweep that empties it.
The lifecycle, in three steps
- The gate names its own remedy. When an emitted-artifact hash moves, or a
gsd-core/workflows/*.md/agents/gsd-*.mdfile grows, and your diff cannot explain it, the failure output tells you which key to use and prints a minimal valid document to paste. Create a NEW fragment named for your issue or PR (<NNNN>-<slug>.json) — never reuse someone else's, and never revive the legacy singletests/emitted-drift-ack.json. - Note the two key spaces. The message says which one applies. An
unattributable hash ripple is keyed on the emitted path
(
skills/gsd-add-tests/SKILL.md); growth is keyed on the bare filename as it appears undergsd-core/workflows/oragents/(explore.md). - Delete the fragment once it has merged (#3078). Every entry is scoped to
the diff that introduced it, so the moment it lands on
nextits prose is already at the base — it is spent and can no longer clear anything, while still owning its path keys. Theguard-no-ack-on-nextjob redsnextand prints the exactgit rm. Run it.
Why the sweep exists
Fragments end the file conflict the single shared document caused. They do not end the key conflict: two ack sources may never name the same path, and that is a hard, loudly-reported error. So a fully-spent fragment left here walls off every path it owns — the next PR to grow one of them can declare it neither in the owning fragment (spent, gates nothing) nor in its own (duplicate). #2914 assumed a persisting fragment was harmless; #3078 measured the cost at 45 fragments owning 403 paths and made the guard sweep them.
A partially spent fragment is deliberately left alone. That asymmetry is what keeps the re-arm route working: appending prose to a live entry re-arms it, and re-arming deliberately costs an actual new sentence — the comparison strips zero-width characters and collapses whitespace precisely so a zero-information edit cannot fake one.
Never pin a fragment in a test
A fragment is deleted the moment it has merged (see step 3 above), so any test
that asserts one exists, or asserts its contents, will fail the instant
guard-no-ack-on-next sweeps it — and that failure has nothing to do with the
behavior the fragment once explained. This has already cost two suites:
tests/emitted-attribution.test.cjs's three #2914 migration pins, and
tests/agent-tracked-source-rule.test.cjs's #3645 growth-ack pin. What a test
may legitimately assert is the BEHAVIOR the ack explains, or the guard's own
verdict (assertNoAllSpentFragments, assertAbsentOnNext) — never the
paperwork.
Do not regenerate anything
There is no baseline file to re-run a generator over; #2724 deleted it. If you find yourself hunting for one, that is the predictable wrong guess.
See CONTRIBUTING.md → "Editing shipped content", docs/TESTING-SUITES.md, and
CONTEXT.md's RULESET.EMITTED_ATTRIBUTION for the full model.