← docs · ADR

Pre-dispatch is mechanically enforced at the close: no wake, no publish

The wake (pre-dispatch: sweep-to-currency + the three briefs, ADR-0008/0014) stops being a prose-only contract. A mechanical entry-driver (tools/predispatch.py) runs the scaffolding — sweep (fail-loud per ADR-0015), briefing, recall brief — and appends a dispatch.open event to the log. The close refuses to publish without a fresh dispatch.open: the gate the producer must exit through becomes the gate that proves it woke. Delta stays an agentic fan (ADR-0001 — judgment, never mechanized) and never gates.

Status

proposed (2026-06-10; Voz ratifies — "next steps" session). Pairs with ADR-0015 (the same incident's other half). Extends ADR-0012 (the shared pipeline gains a mechanical pre-dispatch stage), honors ADR-0008 (sweep triggers), ADR-0014 (three briefs), and the corpus thesis what-to-make-mechanical-the-scaffolding-not-the-cognition (this is its third mechanization, after project-in-publisher and recall-push).

Context

read pipeline.md and scaffold.md — the files that define pre-dispatch — and skipped the entire fan. Zero assemble/delta/recall subagents; the producer went straight to work from the operator's prompt. Meanwhile the close — the one mechanically enforced stage — executed flawlessly (genus check, blind reviews, atomic publish + kernel + signals) via its driver. The asymmetry is the lesson: the mechanized half of the lifecycle ran perfectly; the prose half was skipped by a model that had just read it.

-home-vboxuser literal); the broken sweep would have made an executed wake look pointless ("nothing new"). Neither alone was visible.

glossary's Dispatch entry says every dispatch observes the same effects. Both are prose. The close is code. Contracts survive where they are code.

Considered options

Rejected: tonight's model read the contract first and skipped it anyway. Prompt-strength is not enforcement; this exact failure recurs at the next long context.

the skill). Rejected: there is no mechanical hook at skill entry — skills are markdown read by a model; only tool calls are mechanical. An entry wrapper would be one more instruction to skip.

mechanically is the close (it alone writes artefato.published). Make publish conditional on proof of wake: the entry-driver appends dispatch.open; close.py checks it. Skipping the wake now dead-ends at the moment of publish — visible, attributable, unskippable.

Decision

standalone): (1) sweep to currency (raises on a missing store, ADR-0015), (2) compose the briefing, (3) render the recall brief, (4) append dispatch.open to the Tier-0 log with the sweep yield in its payload (sessions digested, episodes appended — the read:write numerator the Direction wants instrumented rides along free).

last artefato.published** (one wake per publish — a stamp cannot be reused across dispatches). Absent or stale stamp → a refusal that names the violation (no-wake), raised in publisher.publish BEFORE proof verification — the publisher is the one mechanical wall every real publish crosses, so the gate cannot be reached around. (Amended 2026-06-10 review: originally drafted as a close.py strike through the bounce machinery; the publish-seam refusal is strictly stronger — a strike can be re-produced past, a seam refusal cannot — and the tests pin it.) (Amended 2026-07-02, grounding S2 / E1 gate D1: the gate is now identity-held on the canonical path — an id-carrying publish requires an unconsumed dispatch.open whose payload MINTED its dispatch_id (eventlog.wake_fresh_for), checked both at the publisher fast-fail and authoritatively under the eventlog lock; the global newer-than form remains only for legacy id-less callers. The entry-driver prints the id machine-readable (DISPATCH_ID=<id>, first stdout line) for the snippet to carry.)

(ADR-0001/0011). The mechanical floor is exactly the scaffolding: sweep + briefing + recall.

no gate — it publishes nothing; its halt is the contract (CONTRACT C1).

Consequences

briefs); the skill prose describes the wake, the driver performs it — prose stops being the enforcement layer anywhere in the lifecycle.

sweep) at zero extra cost — the read:write instrumentation steer starts here.

degrades honestly per contract, the stamp records the degrade — the gate proves the wake ran, not that the world cooperated.

two wakes (re-running the driver is idempotent and cheap).

reality on three installs) can consume each other's stamp: wake_fresh compares global maxima, so whoever publishes second loses, regardless of stamp order (A's publish can spend B's newer stamp). Accepted: the refusal is loud and names the remedy, and re-running the driver recovers (idempotent). Per-dispatch stamp identity is the known hardening if the race bites in practice. (Hardening SHIPPED 2026-07-02, grounding S2 / E1: wake_fresh_for(dispatch_id) is the identity-held gate on the id-carrying path — each dispatch holds and consumes only its own stamp, an unminted id never publishes; the global race persists only for legacy id-less publishes.)

snippets in the producer SKILL.md set + provision re-render.