enh(#3866): dispatch step and contribution hooks at verify:pre (#3869)

* 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>
This commit is contained in:
Tom Boucher
2026-08-25 18:07:29 -04:00
committed by GitHub
parent 7bbbe495d7
commit 86fa2917d7
7 changed files with 216 additions and 3 deletions

View File

@@ -123,6 +123,10 @@ Gates check a condition at a loop extension point and optionally block progressi
The 12 loop extension points are a **closed, additive-only vocabulary**. Every `steps`, `contributions`, and `gates` entry must use one of these identifiers exactly.
A valid point is necessary but not sufficient: each host workflow decides, per point, which hook **kinds** its dispatch text handles, and a point may dispatch a subset. The registry build derives the real answer from the host workflows (`getWiredKinds()` in `scripts/gen-loop-host-contract.cjs`) and rejects a manifest declaring a kind the point does not dispatch — so an unsupported combination is a build-time error naming the point, the kind, and the kinds that point does cover. It is never a hook that renders and is then silently dropped.
`verify:pre` dispatches all three kinds. A step there is **advisory**: it runs before UAT begins and never blocks it — a precondition that must halt verification is a `gate`. Its `produces` artefact names are consumed **additively** by the verify workflow's `extract_tests` step, which can deepen what UAT covers but cannot suppress a checkpoint. See [Develop a capability](../how-to/develop-a-capability.md#check-the-point-dispatches-your-kind) for the authoring workflow.
| Point | Phase | Position |
|---|---|---|
| `discuss:pre` | Discuss | Before the discuss step executes |