* test(#3866): pin that verify:pre must dispatch every hook kind verify-work.md's verify_pre_hooks step dispatches only `kind == "gate"`, so getWiredKinds reports verify:pre -> {gate} and gen-capability-registry rejects any capability declaring a step or contribution there. The verify lane is therefore closed to capabilities that want to contribute to what UAT covers rather than refuse to let it start. Failing-first: the step, contribution, and exact-kind-set rows are RED; the pre-existing gate row is a green regression pin so the new arms cannot orphan the arm verify:pre already had. Refs #3866 * feat(#3866): dispatch step and contribution hooks at verify:pre verify_pre_hooks dispatched `kind == "gate"` only, so getWiredKinds reported verify:pre -> {gate} and gen-capability-registry's validateHooksWired rejected any capability declaring a step or contribution there. A capability could refuse to let UAT start; it could not contribute to what UAT covers. Add contribution and step arms mirroring execute:wave:post, deferring to references/loop-hook-dispatch.md and carrying its ref.command in-context validation guard ahead of any shell-use prose. A verify:pre step is advisory: it never blocks the start of UAT and an erroring step is routed by its own onError. The gate arm and its check guard are untouched. Give extract_tests an additive consumption seam for the artefacts those steps declare via the existing steps[].produces field -- no new registry field, no new ordering, no invented filename. Manifest-supplied artefact names are validated in-context against an allowlist and resolved only inside PHASE_DIR. With no producing step the derivation is unchanged, pinned by test rather than asserted in prose. Review findings folded in: the artefact-name allowlist (isolated adversarial pass), the artefact-shape contract and the seam-inertness tests (spec axis), and the reference/how-to split so one constraint has one source of truth (standards axis). Closes #3866 * chore(#3866): backfill changeset PR number --------- Co-authored-by: sim <sim@local>
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.