Checkwright v0.22.0

2026-08-06

Checkwright is the verification layer under agent orchestration, and this release is the one that stopped trusting the install path — four separate ways init could silently overwrite something you had edited are closed, the delivery path for a compiled gate binary now ships end to end, publish and install both, and the first ported cohort travels it. It batches eleven iterations behind a deferred tag, so it is wide.

In brief

  • If you have ever edited a vendored file and re-run init, this is the release that stops the next upgrade quietly putting it back. Four distinct paths could do that; all four are closed, and the record of what you own is now permanent rather than something a later payload could drop.
  • Several gates that could pass without checking anything now fail instead. Each was green in a case it should have caught — an exemption satisfied by a coincidence, a CI step skipped rather than linted — which is the worst kind of red to be missing, because nothing tells you it is missing.
  • An evidence run can no longer be clobbered by a second one starting on top of it, and a half-finished run leaves something a later session can actually read.
  • The delivery path for a compiled gate ships in both halves — a release publishes a verified binary and an install places one — and the first ported cohort travels it instead of shipping as shell scripts. If you register a member of that cohort this changes what runs, not only what is possible, and the upgrade section says what to do about it.
  • Wide, but the migration is narrow: one fixture-matching change is the only thing likely to red a tree that was green before.

Tightened gates

  • check-gate-exemption-tasks — tightened, and this is the one most likely to find something in a tree that was green. Every element of an exception list has always had to carry a live until: task or a permanent: reason; what changed is that the liveness half was failing open. It resolved an until: slug by scanning for the name anywhere in the queue’s live span, so a coincidentally bolded ordinary word satisfied it. Resolution now reads bullet lead lines only. The intent is that a temporary exemption with nothing tracking it is a permanent one nobody decided on — and the old form could not tell them apart. Clear a red by reopening the task or converting to permanent: with a reason.
  • check-prose-enum — tightened. Naming two or more members of a declared governed set inside one paragraph must name every member, and the member-match boundary now treats _ as a word character. Previously an underscore-joined identifier satisfied a shorter sibling member by accident, so an incomplete enumeration passed. The intent is that a partial list is indistinguishable from a complete one and goes stale silently; it only reaches you if your declared set has underscore-bearing member names. Complete the list, or use the subset phrasing the gate already exempts.
  • check-action-run-shell — tightened. It has held run: literal block scalars ShellCheck-clean for a while; what changed is what happens at a construct the extractor cannot read. A YAML anchor or alias in a run: value used to be skipped silently and therefore never linted; it is now a loud refusal naming the construct. The intent is that an unlinted step and a clean step used to look identical. Rewrite that step’s run: as a plain literal block scalar.
  • check-docs-link-convention — tightened, with a third rule. It already refused a directory-target relative link and an anchorless kit back-link; a relative link that resolves outside the docs root now reds too. The intent is the gap between your editor and your host — a ../ walk out of the site root resolves locally and 404s for a reader, so the failure only shows up after publishing. Cite the absolute form, or declare an exemption with a reason.
  • check-battery-roster — a new gate. Your runner documentation’s battery-roster block holds name-set parity with EVIDENCE_KIT_SUITES, both directions, and a documented invocation matches what the config actually runs. The intent is a contributor typing a command out of a doc that no longer runs anything.
  • check-producer-liveness — a new gate, and not a battery member: it is wired at the lifecycle stage-entry hook, deliberately, because running it inside the battery would deadlock against the lock a battery run itself holds. It refuses a stage entry while the evidence producer’s lock names a live PID. The intent is a supervisor that starts the next thing because the first one looked finished. A lock naming a dead PID is reclaimed rather than treated as a wall, and an unparseable lock refuses rather than guessing.
  • check-template-registry-parity — a new gate, and a narrow one: a kit’s shipped .list registry template must name exactly the artifacts of its sibling directory, both ways. It is scoped to lists that have such a sibling directory, so a list carrying your own rule content is out of reach. It only bites if you author your own kit.
  • check-gate-substrate-parity — a new gate, dormant until a gate you register dispatches to a binary. It holds one declaration per member, descriptor and subcommand agreeing both ways, a recorded disposition for every substrate-sensitive member, and no implementation source inside the set you vendor. Note the trigger: this release vendors .gate descriptors with gate-sdk, and a descriptor you do not register dispatches to nothing, so it reports what it sees and passes.
  • check-gate-binary-fresh — a new gate, dormant on the same condition. Once a registered member resolving to a descriptor makes a prebuilt binary load-bearing, it holds that the binary was built from the source now in the tree. A tree with descriptors but nothing dispatching to them is a stated clean report rather than a skip, naming both counts, so you can tell “checked and agreeing” from “nothing to check” from “declared but not dispatched”.
  • check-gate-tamper — tightened. The gate-file roster it recognises (DELEGATION_KIT_GATE_FILES) now carries the .gate spelling on its default, so an edit to a ported gate’s declaration is held to the same isolation rule as an edit to a shell gate. Previously the default named check-*.sh only, and a descriptor’s edits escaped the rule entirely. It only reaches you if you run the kit default and a gate you vendor is ported; a consumer that declares its own roster replaces the default outright and restates the globs it wants.
  • check-survey-record — a new gate, inert until you file your first survey. The survey record is a new committed per-iteration surface a stage writes a bought survey into so the next stage can cite it instead of re-buying it; the gate holds each block’s witness — the four keys in order, a non-empty corpus, a non-empty oracle (the literal none being the honest form for a survey no oracle grounds), and a full-sha revision naming a real commit. The intent is that a block missing its witness is silently unusable, which is exactly what the surface exists to prevent. No record, or a record truncated to its header, is a stated clean report rather than a skip.
  • check-payload-claim — a new gate, and silent until you configure a disclosure vocabulary. It asks who owns the claim your documentation makes about what a vendored gate puts in front of the consumer: exactly one governed doc carries a <!-- payload-discloses: <claim-id> --> marker, and no other governed doc asserts a class the marker does not name. The vocabulary is yours — one <claim-id><ERE> line per class through CANON_KIT_PAYLOAD_CLAIMS_CMD — because a spelling of what a payload discloses is your distribution model and no kit ships one. The intent is that a claim restated across install surfaces with no owner drifts a page at a time with nothing watching, and a hand count of where it is stated is the thing with no mechanism to notice the next restatement. Set no vocabulary and it skips clean.

Renamed knobs

None. Several knobs are added — the prebuilt-binary path, the evidence lock, the allowlist breadth probes and the recurrence threshold each introduce one — and none is renamed or removed, so nothing you have set needs to move. One knob’s default changes where it points, which is a behavior change rather than a rename and is stated below.

Behavior changes

  • gate-sdk/bin/run-gate-tests.sh — a fixture’s expect.txt is a conjunction, not a disjunction. Every non-blank line in the file is now its own required assertion; previously any one line matching was enough. Reconcile: this is the one change in this release likely to red a tree that was green. If you wrote a bad/*/expect.txt with several candidate lines, expecting whichever fired to satisfy it, it now fails unless all of them appear. Split the case, or cut the file down to the lines that genuinely always appear. There is no knob and no opt-out; you inherit it by vendoring.
  • GATE_SDK_GRAPH_ARTIFACT — the default output path moves under the gates directory. Reconcile: if you set it explicitly, nothing changes. If you were on the default, the graph projection is written somewhere new and your old copy is now an orphan you can delete.
  • installer/lib/init.sh — ownership is permanent, and four paths that silently overwrote an adopter’s edit are closed. The carve-out that disowned a path the moment a payload stopped shipping it is gone, so ownership now ends only when a path leaves your tree. A file you edited — which init correctly refuses to overwrite — no longer drops out of the lockfile’s roster, which is what made the next upgrade read it as never installed and take it. Every kit’s config-seam template and the gate-sdk seed arm now plan before they copy, rather than overwriting first and reporting the file untouched afterwards. Reconcile: nothing to run, but the losses this prevents are not retroactive — a file already overwritten by an earlier version is in your git history and nowhere else. This is the cluster behind the release’s opener.
  • doctrine-kit/bin/install-doctrine.sh — a declared digest trim survives init. The always-loaded marker block was rewritten wholesale on every run, silently reverting a trim you had declared. It now harvests the trims first and restores them. Reconcile: re-declare any trim you lost; it will hold from here.
  • installer/lib/doctor.sh — path matching is exact, and a clean verdict means clean. doctor matched the lockfile’s roster by path suffix, so a fixture tree inside a kit could shadow your real registry and make it report an installed artifact missing. It also printed a clean verdict line underneath a problem it had just printed. Reconcile: if doctor ever told you something was missing that plainly was not, re-run it.
  • init places a prebuilt gate binary, and it is the last step of the delivery path. init resolves your host to a target, verifies the artifact against a published SHA-256 sidecar before writing it, and records it under its own lockfile key rather than the file roster. An unsupported platform is omitted and declared rather than failing, and a half-present binary/sidecar pair is refused outright. The publish half ships in the same release. Reconcile: nothing — no gate dispatches to a binary yet, so this changes what a payload can carry rather than what your battery runs.
  • gate-sdk/lib/gate.sh — gate resolution splits into a declaration path and a command. A member may now resolve to a .gate descriptor alongside a .sh, and consumer-first shadowing is preserved across both. A declared binary that is missing is exit 2 — a refusal to certify — never a silent skip. Reconcile: nothing unless you shadow gate resolution itself.
  • check-action-pinning and check-action-gh-repo are the first ported cohort — each is now a .gate descriptor and the .sh it replaced is deleted. They are the same checks with the same verdicts; what changed is that a registered one resolves to a compiled subcommand instead of a shell script. The descriptors vendor with gate-sdk whether or not you register either, and a descriptor you do not register dispatches to nothing. Reconcile: if your gates.list names either gate, run init after replacing the kit directories so it places the binary — that is the whole migration, and the sidecar digest is verified before the write. On a platform the target roster does not cover, init omits the binary and declares the omission, which drops those members from your registry rather than reding your battery; re-run init from a covered platform, or on the same one once the roster grows, and the members come back live. If you register neither, nothing changes for you.
  • evidence-kit/bin/run-validate.sh — the producer’s liveness is observable and exclusive. The run claims a lock naming its PID and its iteration, releases it on exit, and reclaims one whose PID is dead. Reconcile: nothing, and it is worth knowing why it is here: this is what lets a supervisor tell an orphaned-but-live producer from a stale lock by reading rather than guessing, which is the difference between waiting and starting a second run on top of the first.
  • delegation-kit/templates/agent-execution.md — the post-commit discipline verb is now verify, and it never re-runs the evidence producer. The old verb collided with the validate stage’s name, and that collision had routed the check onto the producer itself — where re-running it mutates the record the check was supposed to confirm. Reconcile if you vendored or forked this template: re-vendor, or rename the verb and re-route the check off the producer in your copy.
  • lifecycle-kit/templates/lead.md — the next stage is dispatched on a completion signal, never inferred from artifact state. A clean commit, a complete-looking evidence manifest and a green battery can all be true mid-write, and were. Reconcile if you built your own dispatch heuristic on this pattern: switch it to the completion signal; artifact state is not a proxy for done.
  • lifecycle-kit close — a release defers by default and tags on a trigger. Every close still dispositions the release boundary and silence is still not a disposition; what changed is that meeting a bump criterion no longer implies cutting a tag. A third disposition form, deferred:vX.Y.Z, records a release the criteria earned and an operator held, and floors the next qualifying note at that version. Reconcile if you run this lifecycle: your close will now stamp a deferral where it used to tag, and the accumulated declarations ride the next qualifying release — which is exactly what produced the width of this note.
  • lifecycle-kit scope and the gap inbox — a recurrence is counted and can pre-empt the theme. A queue entry carries a recurrence: declaration that the gap-filing tool stamps as it files, and an entry reaching the recurrence threshold is force-included in a scope survey past the theme filter. A new /consult boundary skill lands alongside, and scope’s premise re-verification now aggregates inbound citations from sibling entries rather than reading an entry alone. Reconcile: nothing; the declaration is additive and the threshold has a default.
  • guard-kit/bin/compare-settings-allow.sh — it now asks whether a local allow glob is too broad, not only whether it is redundant. Declared probes are the witnesses: each is a command the report tells you your own glob auto-allows. Reconcile: nothing. The probe set is empty by default, and an empty set omits the section entirely rather than printing a clean one — no completeness is claimed for a check you have not configured.
  • evidence-kit/bin/run-validate.sh — the evidence manifest lands in one write at the end, not row by row. Incremental upserts dirtied the tracked worktree mid-run, which deterministically broke any suite that refuses a dirty tree unless it happened to run first. Rows now batch and fold in configured order, and an aborted run leaves the manifest untouched. Reconcile: nothing, unless you were tailing the manifest for progress — you will now see rows only at the end.
  • canon-kit check-spec-pointer — a version-marker contract: payload is recognized rather than whitelisted. The gate discriminates the payload itself instead of relying on a hand-written exception entry. Reconcile: if you hand-added a whitelist line to get a green battery on such a file, you can drop it; the knob itself is unchanged and still governs everything else.
  • The shared comment-tier and spec-pointer corpus now also walks .gate and .rs files. Several meta-gates read that one walker, so they gain the two file types together. Reconcile: nothing today — a tree with no such files sees no change; it surfaces when you first ship one.
  • context-kit/lib/toolfloor.sh — a Rust toolchain floor joins the probe roster. It is required only to build the gate substrate from source. Reconcile: nothing if you consume prebuilt artifacts, which is the intended path; env-probe will report the floor either way.

Upgrading

Replace the vendored kit directories wholesale at v0.22.0, then re-run init if your gates.list names check-action-pinning or check-action-gh-repo — those two are ported, and init is what places the binary they now dispatch to. Then regenerate the generated artifacts — the pre-commit hook (several of the gates above are tier=precommit and join the generated hook) and the graph projection, which under its default now writes to a new path. Then run the full battery.

The allowed reds. check-gate-exemption-tasks reds on an exemption whose until: slug no longer resolves to a live task lead line — including ones that were passing on a false match. check-prose-enum reds on a paragraph naming part of a governed set with underscore-bearing member names. check-action-run-shell exits 2 on a run: value it cannot read as a literal block scalar, and reds on real ShellCheck findings in the ones it can. check-docs-link-convention reds on a relative link resolving outside your docs root, alongside the two shapes it already caught. check-battery-roster reds when your runner doc and EVIDENCE_KIT_SUITES disagree. check-template-registry-parity reds only if you ship your own kit registry template. check-producer-liveness refuses a stage entry rather than reding your tree, and is not in the battery at all. check-gate-substrate-parity and check-gate-binary-fresh are dormant until a member you register resolves to a .gate descriptor — not until a descriptor is on disk, which this release puts there for everyone — and they report that dormancy rather than going quiet. Neither is in the starting roster init seeds, so they reach you only if you add them. Separately, and not a gate: the expect.txt conjunction change above can red your own fixture suite, which the battery does not run for you.

The behavior changes above are declared for reading, not a mechanical scan. If a gate reds that this note does not name, the upgrade smoke was supposed to catch it first — open an issue, because that is a defect in the release rather than work for you.