feat(receipt): owe a code review over the branch's change, and price it at the ready transition - #880
Merged
Merged
Conversation
|
Warning Review limit reachedNext included review available in 3 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (22)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
wenzowski
force-pushed
the
claude/prose-dispatch-gate-verify-u0fnx3
branch
from
September 5, 2026 23:14
002cf49 to
3e55a8d
Compare
…an be owed CLOUD-1484. `review-dispatched` (CLOUD-472) answers *did a named prompt run over these bytes*, and both its declared subjects are prose — `AGENTS.md` and a tracker body. Nothing has ever asked whether a review ran over the branch's own diff, so a pull request may change every line of `crates/batten` unread. THE DISPATCH INVERTS RATHER THAN REPEATING. CLOUD-472's engine spawns a reviewer, which is what forced a runner, a readiness probe and a prompt channel into a repo-agnostic core. Here Batten refuses, the refusal names what the agent owes, the agent dispatches with its own harness, and `[[mint]]` writes the receipt from the call the boundary was already mediating. That is not CLOUD-1265's refuted producer-writes-outside shape: nobody has to remember to run a producer, because the hook sees the call. `MintKey::Delta` keys a receipt by `git::branch_patch_id` — a wrapper over `cumulative_patch_id` rather than a second computation, so *the same change* means here exactly what `landing` already means by it. A rebase therefore does not re-owe the review and any commit moving code does. The boundary refuses to mint at all while the tree is dirty: a receipt keyed to committed bytes must not be taken in a session looking at something else. `selects_at`/`selects` are the load-bearing pair. Every skill this harness dispatches arrives under one tool name, so `tool` alone discriminates nothing and the row would attest that *something* ran. `requires` cannot reach it — the field is present whichever skill ran, it just holds another value. The selector reads the tool INPUT and `requires` reads the RESULT: the input says which call this is, the result says whether it answered. Two measurements changed the design while building it. A skill's result on this host is the bare string `Launching skill: <name>`, which is not JSON, so `record_mints` gave up before reaching the row — this is the first row whose whole reading is of the input and the repository, so the result is now `null` rather than an early return. And the clean-tree condition is at the WRITE, not in the module: as a conjunct it would let a dirty tree silence the gate, and as a refusal it would be a second opinion where `tree-clean` already owns the question. The receipt attests the DISPATCH, not the completion, and that gap is named in `batten.toml` rather than papered over: the harness returns nothing that would say the agent then did what the skill told it. BREAKING CHANGE: `mint::Declared` gains `key_base`, `selects_at` and `selects`, and `mint::MintKey` gains `Delta`. Both are library-API breaks — the struct is constructible so a downstream literal no longer compiles, and an exhaustive match on the enum no longer covers it. The TOML surface is unaffected: all three columns are optional and a row that omits them is judged exactly as before, which is what an earlier draft of this branch wrongly generalised into `no consumer break`. `semver check` is what caught the difference. Weakens: mint-added mint[code-review] Refs: CLOUD-1484 Admits: 2b23578df77a1290c5cfa5f47512c93cc502705647553d90a63c5c47d4c0e5e4 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:7e2207d701d6c58684d79582e310b37ef9e54367 Admits-epoch: f3ee6646fee52db519d6e2493d1e490f8eb22a3172c03b6bc50fc435199006b4 Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: The gate would not exist. code-review-dispatched is only reachable because a [[rule]] row registers its module and a [[mint]] row keys its receipt; without them the engine change is dead code, and nothing would refuse a branch that changed code with no code review dispatched over it — the defect CLOUD-1484 was filed for. Admits-answer-precondition: A gate is declared as data in batten.toml: the [[mint]] row, the [[rule]] + [[rule.minted]] rows and the [[verdict]] class ARE the change CLOUD-1484 asks for. No [[redirect]] surface can express adding a rule, and non-negotiable rule 1 forbids the consumer facts they carry from living in crates/batten. The write lands in PR #880 where a reviewer reads it beside the module and the engine change it configures. Admits-answer-rejected-route: config read first — I did read batten.toml, and reading is not the change; the row has to be added. patch run first (git restore) — that discards the rows, which is the change itself, not a repair to it. Both routes assume the write was a mistake to undo; here it is the deliverable.
Generated, never hand-edited: `mise run schema` over the four surfaces together, so none can be refreshed alone and drift while the others look current. Refs: CLOUD-1484
`/code-review` was dispatched over this branch, which is what the gate exists to
demand, and it found six things. Four are repaired here; two are bounds and are
now written down where a reader meets them.
THE COULD-NOT-LOOK ARM WAS BACKWARDS, which is the one that would have shipped a
gate refusing exactly what it documents itself as abstaining on. `looked` guarded
`is_object(input.tree.minted)`, but `minted::fields` leaves a declared id ABSENT
when it cannot list the store — so an unlistable store is an EMPTY map, not
`null`, `is_object({})` holds, and every fresh clone and every CI runner would
have been refused. The guard is per-id now.
THE CLEAN-TREE CONJUNCT AT THE MINT WAS UNSATISFIABLE IN THIS REPOSITORY.
`git::uncommitted` reimplements status and does not skip a GITLINK, where
`walk_blob_ids` explicitly does, so an uninitialised submodule counts as changed
forever: `git status` reports this tree clean while `changed_paths` returns
`{"tests/bats"}`, a mode-160000 entry. `Ok(0)` could never hold and the receipt
could never be minted. Withdrawn rather than repaired in place — it was also a
second authority over a question `tree-clean` owns, and a reviewer reads the
working tree anyway, so refusing to record a dispatch taken over uncommitted work
attests less than happened. The primitive's defect is CLOUD-1500.
`delta_sources` DOES NOT NARROW WHAT A MODULE SEES. `rules.rs` builds ONE
`base_delta` from the union of every row's globs, and six rows here declare
`["**"]`, so a workflow-only or schema-only branch owed a code review. The module
carries the reviewable prefixes itself.
A DECLARED MUTATION WAS NON-DISCRIMINATING. `no-identity-priced` named a case
whose `code-changed` was empty, so `owed` already excluded the mutated conjunct
and the mutation would have survived the sweep. The fixture now leaves the edit
UNCOMMITTED, which is the one state where the working-tree delta says code moved
and the committed range says nothing did.
Two bounds stated rather than fixed: `owed` is a tip diff and `subject` is a
merge-base one, so a stale branch can owe a review until it rebases; and the
identity covers the whole change, so a prose commit on a branch that also carries
code re-owes it. Both err toward owing a review that is not strictly due, and
neither can produce the other direction, which is a branch landing unread.
Refs: CLOUD-1484
…usal The per-id guard made the old case's expectation wrong, and it was wrong for a reason worth keeping: `minted` is keyed by DECLARED ROW, and `minted::fields` inserts an entry for every row whose store it could list — so an id missing from the map means the engine never looked for it, never that it looked and found nothing. Refusing there is a verdict about the engine's reach wearing a verdict about the branch. The claim the case meant to make is already `test_a_receipt_over_another_change_ does_not_answer`, where the id IS present and the subject belongs to another change. Refs: CLOUD-1484
`lint:fmt` refused the `let (Some(path), Some(expected)) = ...` line in `mint::selects`. No behaviour changes; prettier reflowed `.claude/rules/policy-modules.md` in the same pass. Refs: CLOUD-1484
`/code-review` was dispatched again after the enrolment commit moved the patch identity — which is the gate demanding its own re-attestation — and this time every finding was inside this branch's diff. THE WRITER WAS NEVER DRIVEN, and that is the one that would have shipped a gate refusing forever with a green suite. Every case hand-wrote `code-review.<id>` and called `branch_patch_id` directly, so `mint_receipts` was never invoked and the two halves were never shown to agree: a different key base, separator or authority root would have left the reader looking somewhere the writer never wrote. That is the `with input as` class one layer over, in the suite of the module whose own header refuses it. `mint_receipts_for_test` drives the real boundary, and the wrong-skill arm is there too, so the selector is proven over the engine rather than over `selects` alone. `batten.toml` ASSERTED A GUARANTEE THIS CHANGE HAD WITHDRAWN. The block still said the boundary refuses to mint while the tree is dirty; that conjunct was removed earlier in this same branch because `git::uncommitted` counts an uninitialised gitlink as changed forever (CLOUD-1500), so it could never hold. A false guarantee in the policy authority is worse than a missing one. `branch_patch_id` PROMISED could-not-look AND RAISED. `head_commit(dir)?` propagates where the doc says `None` — latent while both callers swallow it, and a trap for the next one that reads the doc instead of the body. `base_delta` PAID FOR THE IDENTITY UNCONDITIONALLY while every sibling git fact is declaration-gated. It is a second repository open, a merge-base walk and a tree diff, on every `check` declaring any `delta_sources` — which is most of them — for a field one module reads. Gated on a row declaring `minted`. `mint::selects` AND THE FOUR NEW LOAD REFUSALS HAD NO TESTS. Loosening the equality to a prefix, or returning true on an unresolvable path, reddened nothing — and either makes the row mint on every skill dispatch, which is the false green the column exists to remove. Two findings are bounds rather than fixes, stated where a reader meets them: `requires = []` has no success predicate because `Declared` carries no `is_error` column, so an errored dispatch still mints; and `reviewable` restates the row's `delta_sources` with nothing holding the two in agreement. Refs: CLOUD-1484 Admits: 0a91c088ab4d93b4a6e1bd01f340b1d94d756cb57f3e003a56baf2f07593684c Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:58efe536b4d39ef6aac43e52def6da3ea67e74b4 Admits-epoch: 976d2841a1864bc485ab2531d686611f0708820fc8f307ba0f2b965502c679cf Admits-author: alec@wenzowski.com Admits-prev: 2b23578df77a1290c5cfa5f47512c93cc502705647553d90a63c5c47d4c0e5e4 Admits-answer-lost: A false guarantee stays in the file every gate reads. A reader checking whether a dirty tree is refused would find batten.toml saying yes and the engine doing nothing, which is worse than no statement at all — it is the shape this repository records as a false assurance, and the code review that found it is the only thing that would have caught it. Admits-answer-precondition: The write is a CORRECTION to prose in batten.toml that asserted a guarantee this branch had already withdrawn: the block said the boundary refuses to mint while the tree is dirty, and that conjunct was removed earlier in this same branch because git::uncommitted counts an uninitialised gitlink as changed forever (CLOUD-1500). No [[redirect]] surface can edit a comment, and the claim is in the policy authority where a reader meets it. It lands in PR #880 beside the code that withdrew the conjunct. Admits-answer-rejected-route: config read first — reading is how the defect was found; the false sentence still has to be replaced. patch run first (git restore) — that discards the correction and restores the false guarantee, which is the opposite of the repair.
`clippy::expect_used` is denied crate-wide, and the new `mod tests` cases build their fixtures with `serde_json::from_value(...).expect(...)`. `render.rs`, `contract.rs` and `admission.rs` each carry the same allow on the same attribute — the lint is about reachable library paths, and a test fixture that cannot be constructed should fail the test loudly. Refs: CLOUD-1484
The gate demanded a third review after the floor move shifted the patch identity, and every finding was again inside this branch's diff. THE NULL FALLBACK WIDENED `satisfied` VACUOUSLY, and it is mine. `record_mints` passes `Value::Null` where it used to return early, so a row reading none of the result can mint from a call whose answer carried no JSON. For a `requires = []` row whose BODY reads the result, the success test is then vacuously true, every path piece records the could-not-look token, and under `MintMode::Replace` that all-`-` record overwrites a good receipt. No landed row is shaped that way, which is exactly why nothing would have caught it. `render` now withholds a record when the result is null and any piece reads it; `Piece::reads_the_result` is exhaustive with no wildcard, so a variant added later fails to compile rather than classifying itself as reading nothing. THE `patch-id` GATING WAS UNSOUND AND IS WITHDRAWN. A previous review asked for it to be declaration-gated like every other git fact, and the gating shipped keyed on `!rule.minted.is_empty()` — which is not the declaration that reads the fact. A module reading `patch-id` with no sibling `[[rule.minted]]` row would see `null`, decide nothing, and exit 0; for `code-review-dispatched` itself, dropping the minted block would have silenced the gate through two channels at once. The merge-base walk is paid again and the cost is stated rather than hidden. Gating it honestly needs a column naming which facts a row reads, which is a bigger change than this one. `selects` NAMED THE WRONG SURFACE. Its parameter and doc said RESULT while every caller passes the INPUT, contradicting the call site — the wrong-surface trap that produces a row matching nothing and a gate that denies forever. The unit cases could not catch it, because they supply the object themselves. A BLANK `selects` LOADED CLEAN where a blank `selects_at` was refused. It matches no value any host sends, so the row mints nothing and its gate denies forever with a remedy that cannot clear it. Both halves are refused now. Two false claims are corrected where a reader meets them. The module METADATA said it could not under-owe: it can, because the identity is over COMMITTED bytes, so an uncommitted code edit leaves the receipt valid and `batten check` reports clean over unreviewed work — `tree-clean` is what covers that, and only on the landing path. And the verdict route names `/code-review` without saying that a TYPED slash command may mint nothing on a host that expands it with no tool call, which is a remedy the reader cannot walk. One finding is left as a bound: `key = "delta"` is the first unbounded keying — one receipt per patch identity, never pruned, and `minted::fields` reads every matching file on each `check`. Refs: CLOUD-1484 Admits: 486f8518288e82b9b457c9df8dd28a0d34b8f705466d0b2952eb2b6f7e1953b6 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:d874abf892e4207f698df9ad33c4d3dd6114e20c Admits-epoch: 5b699dcd1dcec80408ccacd8807b5e803d6068c2530bc97924477d4bfd87c93b Admits-author: alec@wenzowski.com Admits-prev: 958bfb7cb712ab26af8d9194bc7fd64b9fa29ad2822ad226bedbd96b1e87cd14 Admits-answer-lost: A route that cannot be walked stays in the file as though it can. The reader who hits it is a person at a keyboard following the refusal's own instruction, and the loop has no exit visible from the message — which is strictly worse than a class with no route at all, because it reads as a way out. Admits-answer-precondition: Two corrections to prose in batten.toml, both found by the code review this branch's own gate demanded. The [[verdict.route]] block names /code-review as the remedy without stating that a TYPED slash command may mint nothing on a host that expands it with no tool call, so the gate re-denies with a remedy the reader cannot walk. No [[redirect]] surface can edit a route's commentary, and the claim lives in the policy authority where a reader meets it. It lands in PR #880 beside the module it describes. Admits-answer-rejected-route: config read first: reading is how the defect was found, and the sentence still has to be written; a route stated wrongly is not repaired by re-reading it. patch run first (git restore): that discards the correction and restores the unqualified route, which is the defect rather than the repair.
…ier could not see it
The fourth review found the defect that would have shipped this gate broken in
production while every case here stayed green.
`record_mints` OPENED WITH `if envelope.result.is_null() { return; }` — three
lines above the comment this branch added saying a null result is NOT an early
return. Both cannot be true, and the code won. A `Skill` post-tool whose host
sends no `tool_response`, or sends `null`, took that return: no receipt written,
`code-review-dispatched` denies, and the refusal names `/code-review` as the
remedy for something the agent has already done. A gate whose stated remedy
cannot clear it is worse than an absent one.
THE TIER COULD NOT CATCH IT, WHICH IS THE PART WORTH RECORDING.
`mint_receipts_for_test` enters BELOW this guard, so the compiled cases prove the
writer and reader agree about a key and say nothing about whether the boundary is
reached at all. That is the seam a test which drives the function directly always
leaves: the cases were right and the entry point was not, and only a reader
looking at the call site would have known.
The guard is gated on `envelope.event != Event::PostTool` now, which is the
question the line was actually asking. Nullness of the result was never that
question, and reading it as such is what made the two statements disagree.
`mint_receipts`' DOC COMMENT WAS ORPHANED by this branch's own test shim, which
landed between the comment and the signature — so the boundary is undocumented
and the helper carried four paragraphs describing it. Restored.
`validate` REFUSED HALF A SYMMETRY. A `key_base` on a row not keyed `delta` is
refused; a `key_from` on a row not keyed `named` was ignored, though it is read
by nothing in exactly the same way. Both refused now, with a case pinning each
direction.
One finding stays a bound rather than a fix: `key = "delta"` accumulates one
receipt per patch identity with no prune, and `minted::fields` reads every
matching file on each `check`.
Refs: CLOUD-1484
…the filter
The fifth review found that the previous round's repair was itself wrong, in the
direction the previous round's repair was written to fix.
`envelope.event != Event::PostTool` IS THE RIGHT QUESTION ASKED IN THE WRONG
PLACE. `record_post_tool` is `record_mints`' only caller and `lib.rs:8319` already
gates on exactly that, so the guard was dead code — and adding it DELETED the only
cheap filter without replacing it, while the comment above claimed one was there.
That is the same defect as the round before it, one layer over: a comment
asserting what the code does not do, caught by a reader rather than a test.
Both guards are gone. The event is decided at the call site, and the config load
is paid on every post-tool event — the cost of admitting a dispatch whose result
carried nothing, stated rather than bought back with a predicate that cannot
express it. A cheaper filter would have to know which tools any row names before
the rows are loaded, which is the thing being loaded.
THE EMPTY-SELECTOR REFUSAL ONLY TRIMMED. `selects = "code-review "` loaded clean,
compared against a value no host sends, and left the row minting nothing — the
same silent dead gate the check was added to close, one space away. Refused
rather than trimmed: trimming decides on the author's behalf which of two
readings they meant.
`mint_receipts_for_test` HARD-CODED `None` FOR THE GRAMMAR where the boundary
passes one built from the resolved policy, so the helper whose entire
justification is fidelity diverged from the boundary for `{authority:…}` pieces.
It takes the parameter now, and the tier passes `None` explicitly with a note, so
a case that adds such a piece has to decide rather than inherit.
Refs: CLOUD-1484
…esh clone `delta := input.tree["base-delta"]` was dereferenced in `owed` and `subject` with no `is_object` guard, while its sibling `looked_at` guarded `input.tree.minted` for exactly that reason. `rules.rs` projects `base-delta` as `null` when the base ref does not resolve, indexing `null` is a hard evaluation fault in Rego rather than a silent miss, and a fault takes the whole bundle down — every predicate in it, not only this one. A fresh clone with no `origin/main` is the reachable instance, and the module's own metadata already says so about `minted`. Guarded, so an unresolvable base leaves `delta`, `owed` and `subject` undefined and every arm goes quiet — the could-not-look reading this module already takes for an absent identity. `test_a_delta_the_engine_could_not_build_does_not_fault` is the case; the load-time tier goes 757 → 758. Found by the code review this gate exists to demand, over its own diff. Admits: 4cdb9099ad184ad0242ff2b6f369c089d334cf1aee433a4c42b131f0b39ec09d Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: policy/code-review-dispatched.rego Admits-anchor: call:8bbc08550d75be92205e1037158331621a9352e0 Admits-epoch: af7bc401f010623937d6e9319c981c2b4d33dcf318281035ed45e57da25037a9 Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: Without it, `delta := input.tree["base-delta"]` is dereferenced unguarded while its sibling `looked_at` guards `input.tree.minted` for exactly that reason. On a checkout whose base ref does not resolve the engine projects `base-delta` as null, indexing null is a hard evaluation fault in Rego, and the fault takes the whole bundle down — every predicate in it, not just this one. The gate this branch exists to add would be off on any fresh clone with no origin/main. Admits-answer-precondition: The change is a Rego predicate guard inside a policy module, and no declared surface can express it: batten.toml holds config rows, not module bodies, and git restore reverts rather than edits. Writing the module directly is the only route, and the guard lands in this branch's diff where a reviewer reads it. Admits-answer-rejected-route: `config read first` (batten.toml) does not apply: a `[[pattern]]` or path-set row cannot carry a Rego body, and the missing guard is a clause in the module, not a value the config supplies. `patch run first` (git restore) does not apply either: it restores committed bytes, and the committed bytes are the ones carrying the defect. Refs: CLOUD-1484
…be owed at a transition CLOUD-1547. The read side of `MintKey::Delta`: a `receipt` rule may now declare `key = "delta"` with a `key_base`, and its subject is the identity of this branch's whole change against that ref. The three existing keyings price a review at the wrong rate. `head` expires on every commit AND every rebase, so a loop that rebases per lap re-buys the receipt each lap for a change that did not move — the false-positive rate `branch`'s own doc says gets a guard bypassed. `branch` expires on nothing but a restart, so a branch reviewed once may then push anything. Neither is "these bytes, however they were replayed". `receipt::delta_subject` resolves through `git::branch_patch_id` — the same function the mint files under. That shared call is the agreement: a second derivation of "the same change" would be free to disagree with the mint's about exactly the rebase this keying exists to survive. Refused at load in both directions: `delta` with no `key_base` has no second side to diff, so it would read no file for any call and deny everything, which is the direction that gets a guard switched off; a `key_base` on any other keying is a column that reads as configured and is never consulted. An unresolvable base or an EMPTY diff answers `Missing` rather than could-not-look, which is the opposite of what the `branch` and `named` arms do and is deliberate: could-not-look allows, so a branch whose base does not resolve could push anything. A branch with nothing to review is refused loudly and cleared cheaply. `Policy::receipt_key_base_for` carries the prefix because `key_base_for` already means `requires_key`'s base — one ref for the whole call, answering since which commit evidence is looked for. Two refs, two questions; a reader reaching the wrong one gets a plausible value and the wrong receipt. `--key delta` is refused from `receipt status` as `--key named` is, one reason further on: the base is the declaring row's, and defaulting one would answer about a base no row named. Refs: CLOUD-1547
…commit CLOUD-1547. `code-review-dispatched` was a tree rule, so `batten check` decided it under `verify` — which is what an agent runs repeatedly WHILE iterating. Every fix commit moved the patch identity and owed another review, and the review that found something produced the fix that owed the next one. Measured on the branch that built it: six dispatches in one session, each provoked by the fix for the last, each charged over bytes the deterministic gates had not judged yet. The keying was right; the enforcement POINT was wrong. `ready-needs-review` is a `delta`-keyed `receipt` row over `gh pr ready` — the event that spends CI minutes. A rebase-only lap resolves the same identity and buys nothing; a content change files the next review under a name the row does not look up. The module, its compiled tier and its `MUTANT_GATES` entry are deleted, and `patch read never` is withdrawn naming the successor rather than left orphaned. Its route is kept verbatim: what satisfies the demand is unchanged, only where the demand is made. `crates/batten/tests/it/review_receipt_delta.rs` is the tier, over real `adjudicate` calls. The rebase and content-change cases are an anti-vacuity pair in the strict sense — a `head` keying fails the first and a `branch` keying fails the second — so a suite carrying only one is satisfied by the wrong column. Two things this does NOT do, written into `batten.toml` rather than discovered: `push-needs-review` is not committed. It is what would freeze the readied head, and pricing to a transition is only sound if the head cannot change under it. A `receipt` row selects on a command pattern, so it fires on every push in the repository, while what it wants to refuse is a push to a branch whose PR is already ready — and `forge.rs` carries no draft field, so that narrowing is unsayable. Measured: it denied `forced_push.rs`'s benign fixture push, and 15 test files drive a push unrelated to review. The gap is CLOUD-1446's and closes in CLOUD-1548, where a declared loop can name the ready state. The tier keeps a push case over its own config, so the keying is shown not to be pattern-specific. And retiring the module widens the gate. It gated on `code-changed` intersected with the reviewable prefixes, so a prose-only branch owed nothing; a `receipt` row has no such column, so a documentation-only branch now buys one review before readying. Deliberate and cheap at one dispatch per branch, but a widening. `key_base` is classified in the column census as not fact-bearing — an unclassified column is declarable on a mediated row and read by nothing, which is what that census exists to refuse. Admits: eb43570d9ba0d9204b29ca2eb2d34cd74707723ab246201cb0e06a0db187cc7f Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:5ffde3395ad0846c5a344da6047ff073406c4505 Admits-epoch: af7bc401f010623937d6e9319c981c2b4d33dcf318281035ed45e57da25037a9 Admits-author: alec@wenzowski.com Admits-prev: 486f8518288e82b9b457c9df8dd28a0d34b8f705466d0b2952eb2b6f7e1953b6 Admits-answer-lost: Without it CLOUD-1547 has an engine keying nothing declares. `ReceiptKey::Delta` would be a column no row uses — the dead-gate class this repository records, shipped deliberately rather than by accident — and `code-review-dispatched` would go on pricing a review per commit on the tree surface, which is the defect the keying was built to remove and which cost six model calls in one session on this branch alone. Admits-answer-precondition: The change is two `[[rule]]` rows and a `[[verdict]]` withdrawal in the policy authority itself, plus dropping the retired `[[rule]]` and its module registration. No declared surface can express a rule row: batten.toml IS the surface, `batten init` refuses an existing file, and `git restore` reverts rather than edits. Writing it directly is the only route, and the rows land in this branch's diff where a reviewer reads them. Admits-answer-rejected-route: `config read first` (batten.toml) is the path being written, so it names itself and resolves to nothing. `patch run first` (git restore) does not apply: it restores committed bytes, and the committed bytes are the ones carrying the per-commit pricing. Refs: CLOUD-1547
…t a git init chain `fixture-forks` refused `review_receipt_delta.rs:104` and was right: the fixture hand-rolled `git init` + `add` + `commit` + `update-ref`, which is the habit CLOUD-1419 measured at 1,819 init processes and 4.49s over one run. `common::Fixture` copies a published template instead of forking `git init`, and its `base_commit` already pins `refs/remotes/origin/main` — the very ref these rows name — so the chain was not only expensive but redundant. 4881/4881 unchanged. Refs: CLOUD-1547
… here CLOUD-1547 is implemented and closed by this PR, so its §1 names these eight paths because they are the files this change edits. That is precisely what `filed-here.rego`'s `closes` arm exempts, and the exemption cannot fire: the `pr-closes` record is minted from a `gh pr view --jq .body` call and there is no `gh` on this host, so `closes` is empty. Empty is not `closes_unreadable` either — nothing attempted the call, so the blocked store has nothing to record. CLOUD-1126 named that shape; CLOUD-1481 carries the residual. The class's own override route is spent without suppressing. Nine admissions were issued and spent against these subjects and the findings did not move. The consumption is NOT missing — `admission::Anchor::Finding` exists for exactly this and `apply_admissions` suppresses stored findings by fingerprint. What fails is the anchor resolver: `lib.rs:5428` matches stored findings on `(rule, path)` and falls back to `Anchor::Call` on zero matches, so the spend produced `Admits-anchor: call:2dee1726…` for a tree finding, and nothing queries a call anchor for one. The ambiguity arm refuses precisely to avoid "an override that appears to work"; the zero arm walks into it. CLOUD-1551 carries that, with why the match returned zero as its open question. So all four declared routes are unavailable here: the fix is committed, the body says `Closes CLOUD-1547` and cannot be read, filing from a clean tree cannot apply to a row already filed and already implemented, and the override binds an anchor nothing reads. A `[[waiver]]` is the blunt instrument that route existed to avoid, which is why it names both rows, carries a reason, and expires 2026-10-11 rather than switching the rule off. It names `filed-here` rather than `filed-over-own-diff` because a waiver keys on a rule id and that is one of the two predicates the rule publishes, so it is wider than intended and also silences `filed-unrefined`. Stated on the row. Admits: 09521541a8bd8da59b93ddfb949fd6af188f81dfae43ca4b5775397dd437e9a2 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:2dee17263fa63c4951cc4b4b3e802d08f6e20195 Admits-epoch: 1ab5aef43a9bb3a8da9b8c2adf82a92d26c626ded0af816a694267fc8337d576 Admits-author: alec@wenzowski.com Admits-prev: 36aa4e297aebc8c5fa0490acac99a36be208cd6938f3ff623419c2ff6c939433 Admits-answer-lost: A committed waiver whose stated cause is false. It says filed-here.rego has no admission conjunct and nothing reads the store; measured, admission::Anchor::Finding exists for exactly this and apply_admissions suppresses stored findings by fingerprint. The real cause is the anchor resolver falling back to a call anchor on zero matches. A waiver is the one artefact whose job is to say why a gate is suppressed and when to stop, so a wrong cause in it is the false-pointer failure scanning.md records, committed. Admits-answer-precondition: The change corrects the reason string and comment on a [[waiver]] row in the policy authority. batten.toml IS the surface a waiver lives on; batten init refuses an existing file and git restore reverts rather than edits, so writing it directly is the only route. It lands in this branch diff where a reviewer reads it. Admits-answer-rejected-route: config read first names batten.toml, the path being written, so it resolves to nothing. patch run first (git restore) would restore committed bytes that do not carry the waiver at all. Weakens: waiver-added waiver[filed-over-own-diff]
…at suppresses CLOUD-1553. `waiver-names-no-rule` resolved a waiver's `rule` against `config.rules` alone. A policy rule publishes PREDICATE ids from its module and a policy finding carries the predicate id, so `waiver::apply` and `config lint` disagreed about which name a `[[waiver]]` holds — and each was self-consistent: rule = "filed-over-own-diff" suppresses (8 audit lines) refused by the lint rule = "filed-here" suppresses nothing clean to the lint Measured on this tree minutes apart by changing only that value. For any policy rule whose module publishes an id of its own, no value satisfied both halves, and the spelling the lint blessed was the dead one — which is the "exemption somebody is relying on" CLOUD-208 opened this smell to catch, produced by the catcher. The bundles were already in hand for the set analysis below and simply never consulted. `Bundle::declared` is the module's own published set — the same authority `attribute` resolves a violation's id against — so this acquires nothing and adds no second list that can drift. The fix deliberately does NOT go the other way. Making `waiver::apply` match the rule id instead would let a waiver over `filed-here` suppress every predicate the module publishes, so waiving one finding class would silently waive the rest. That is a weakening of the raise-only invariant, and it is the widening this branch had to accept and write a comment about before the repair. Second arm: bundles are EMPTY when a module will not load, which a config judged away from its own tree always is — `repo_with_committed_config` is exactly that, the committed `batten.toml` with no `policy/*.rego` beside it. With no bundle to ask, a predicate id is indistinguishable from a typo, so the smell abstains rather than resuming the inversion. Narrow on purpose: it abstains only where the config declares a policy rule and nothing resolved, so a config with no policy rules keeps the smell's full reach. The `filed-over-own-diff` waiver added last commit takes the narrow spelling now. It suppresses that predicate alone and leaves `filed-unrefined` deciding, so the widening its comment apologised for is gone. A wrongly refusing gate is a defect to repair rather than a ticket to file, so this lands here rather than being left for CLOUD-1553 to pick up. Admits: 0a3b4f556f824a4a46783cdac0ad57aae4141253de32752d0a30ac7e5173175c Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: batten.toml Admits-anchor: call:3b21e71b75f10a06de5851c15536534d709d2f64 Admits-epoch: ff75259a4c4c2868d11c45ce05bb1768588122d48b8579c106ae1495d71c21a0 Admits-author: alec@wenzowski.com Admits-prev: 09521541a8bd8da59b93ddfb949fd6af188f81dfae43ca4b5775397dd437e9a2 Admits-answer-lost: A committed waiver whose stated cause is false. It says filed-here.rego has no admission conjunct and nothing reads the store; measured, admission::Anchor::Finding exists for exactly this and apply_admissions suppresses stored findings by fingerprint. The real cause is the anchor resolver falling back to a call anchor on zero matches. A waiver is the one artefact whose job is to say why a gate is suppressed and when to stop, so a wrong cause in it is the false-pointer failure scanning.md records, committed. Admits-answer-precondition: The change corrects the reason string and comment on a [[waiver]] row in the policy authority. batten.toml IS the surface a waiver lives on; batten init refuses an existing file and git restore reverts rather than edits, so writing it directly is the only route. It lands in this branch diff where a reviewer reads it. Admits-answer-rejected-route: config read first names batten.toml, the path being written, so it resolves to nothing. patch run first (git restore) would restore committed bytes that do not carry the waiver at all. Refs: CLOUD-1553
The ninth code review over this branch found it, and it would have shipped a gate
that refuses everything.
`delta_subject` opened `Path::new(".")` where the mint opens the repo root.
`batten hook` runs with whatever cwd the harness had — routinely a subdirectory,
sometimes outside the repository — so the read side reached a different
repository or none, `branch_patch_id` answered `None`, and because an absent
identity is `Missing` rather than could-not-look at that call site, a
`delta`-keyed row refused EVERY call whatever receipt was on disk.
The two halves disagreeing about *where the repository is* is the same class as
the two halves disagreeing about *what the same change is*, which the function's
own doc already warns against one paragraph up: the mint and the reader must
resolve one identity. They call the same function; they now open the same tree.
Invisible to all eight existing cases because they run at the fixture root, so
the new case runs `adjudicate` from `crates/batten/src` and asserts the same
receipt still answers. It checks the root arm first, so a refusal from the nested
arm is attributable rather than ambiguous.
Refs: CLOUD-1547
…imes The tenth code review over this branch found it. `delta_subject` was recomputed at each of the three sites that want it — the validity arm, the `max_age` branch and the `requires_field` branch — and each call is a repository open plus a merge-base walk and a tree diff. A `delta`-keyed row declaring both `max_age` and `requires_field` therefore paid three of them on every mediated call, against the ~100 ms budget `perf-assert` holds this path to. Hoisted to one binding per CHECK, which is the narrowing the arm's own comment already argued for: the base is a property of the ROW, so two rows keying on different bases must not answer from whichever was read first — but within one check there is exactly one base and therefore exactly one identity, so recomputing it bought nothing. Only a `delta`-keyed check pays anything: every other keying leaves the binding `None` without opening a repository, which is the same cheap-when-irrelevant posture `max_age` takes one arm below. No verdict moves — the `Option` is the same three-valued answer each site already read, so this is the identical decision made once. Refs: CLOUD-1547
…hoist 1adec082 moved the identity into a per-check binding and left the arm saying it is "NOT resolved once above", which is now the opposite of what the code does — and it attributed the two-rows-two-bases hazard to the arm, where it is actually handled by the per-check map plus the key guard. The eleventh review over this branch caught it. A comment that tells the next reader the opposite of the code is worse than none here in particular: the whole arm exists because its answer is deliberately the inverse of its two neighbours, so a reader arriving to check that reasoning is exactly the reader this would mislead. Refs: CLOUD-1547
… the line ceiling CLOUD-1553's two arms pushed `smells` to 105 lines against clippy's 100, and the waiver loop is the self-contained half to lift out: it reads the waiver table, the rules those waivers name, the predicate ids the bundles publish, and the date — nothing else in `smells` and nothing after it. No verdict and no ordering move. The caller extends at the position the loop occupied, and the whole list is sorted by (at, id) afterwards regardless, which is what keeps the report byte-stable under §6. Refs: CLOUD-1553
…tespace The literal was written as one long string with the source indentation folded into it, so the emitted census text read "the classifier rides the advisory channel". Every neighbouring entry uses backslash-continued strings; this one now does too. Cosmetic in the source and not in the output: the census is a document a reader is meant to read, so multi-space gaps in it are a defect in what the engine says rather than in how it is typed. Refs: CLOUD-1547
wenzowski
marked this pull request as ready for review
September 7, 2026 02:23
wenzowski
force-pushed
the
claude/prose-dispatch-gate-verify-u0fnx3
branch
from
September 7, 2026 02:24
3e55a8d to
6eb08e1
Compare
wenzowski
marked this pull request as draft
September 7, 2026 02:24
|
❌ The last analysis has failed. |
wenzowski
marked this pull request as ready for review
September 7, 2026 02:49
Contributor
Author
|
/fast-forward Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes CLOUD-1484.
Closes CLOUD-1547.
Closes CLOUD-1553.
Why
review-dispatched(CLOUD-472, #826) already answers did a named prompt run over these bytes, keyed by two digests and refusing absence only. Both its declared subjects are prose —AGENTS.mdand a tracker body. Nothing has ever asked whether a review ran over the branch's own diff, so a pull request may change every line ofcrates/battenunread.Three measurements on
38fa830, stated rather than inferred:AGENTS.mdand the refinement body; neither is the diff;review::record_pathwrites under$GIT_DIR, which is empty on every fresh clone and every CI runner;batten-review-runneris on no PATH and has no implementation in the tree, soreview::resolvereturnsNoRunner, the whole fact collapses to could-not-look, andrequired contains "ready-pressure-test"has never refused anything.What changed
The dispatch inverts rather than repeating. CLOUD-472's engine spawns a reviewer, which is what forced a runner, a readiness probe and a prompt channel into a repo-agnostic core. Here Batten refuses, the refusal names what the agent owes, the agent dispatches with its own harness, and
[[mint]]writes the receipt from the call the boundary was already mediating. Nothing about that harness reachescrates/batten:tool,selects_atandselectsare stringsbatten.tomlsupplies and the engine matches.This is not CLOUD-1265's refuted producer-writes-outside shape.
tool-verdict's store went measured dead because somebody had to remember to run the producer; nobody has to remember here, because the hook sees the call.MintKey::Delta+key_basekey a receipt bygit::branch_patch_id— a wrapper over the existingcumulative_patch_id, not a second computation, so the same change means here exactly whatlandingalready means by it. A rebase does not re-owe the review; any commit moving code does.selects_at/selectsnarrow a row to one call. Every skill this harness dispatches arrives under one tool name, sotoolalone discriminates nothing. The selector reads the tool input,requiresreads the result.base-deltagainspatch-id, so a module can name the change without re-deriving an identity from the path lists.ReceiptKey::Deltais the read side, so areceiptrule can be keyed the same way the mint files.ready-needs-reviewis that row.The gate was run over itself fourteen times, and that is the finding
/code-reviewwas dispatched over this branch — the obligation the gate imposes — repeatedly, because each fix moved the patch identity and re-owed one. Four defects from round one would have shipped a broken gate:lookedguardedis_object(input.tree.minted), butminted::fieldsleaves a declared id ABSENT when it cannot list the store — so an unlistable store is an empty map,is_object({})holds, and every fresh clone and every CI runner would have been refused, precisely the arm the METADATA promises is silent.git::uncommitteddoes not skip a GITLINK, so the uninitialisedtests/batssubmodule counts as changed forever:Ok(0)could never hold and the receipt could never be written. Withdrawn rather than repaired in place — it was also a second authority over a questiontree-cleanowns. The primitive's own defect is CLOUD-1500.delta_sourcesdoes not narrow what a module sees.rules.rsbuilds ONEbase_deltafrom the union of every row's globs, and six rows here declare["**"].no-identity-pricednamed a conjunct another already excluded, so it would have survived the sweep.Later rounds found the two that mattered most, both invisible to every test then present:
delta := input.tree["base-delta"]was dereferenced with nois_objectguard while its siblinglooked_atcarried one.rules.rsprojectsnullwhen the base does not resolve, indexingnullis a hard Rego fault, and a fault takes the whole bundle down — so on any fresh clone with noorigin/mainthe gate would have been off, silently, along with every other predicate in it.delta_subjectresolved the patch identity from"."while the mint resolves it from the repo root.batten hookruns with whatever cwd the harness had, so from any subdirectory the read side opens a different repository, gets no identity, and — because an absent identity isMissingrather than could-not-look here — refuses every call whatever receipt is on disk. Every existing case ran at the root and so could not see it;the_identity_is_resolved_from_the_repo_root_and_not_the_cwdis the one that does.And then the loop itself was the defect (CLOUD-1547)
Fourteen dispatches in one session is not a gate working, it is a gate priced at the wrong point.
code-review-dispatchedwas a tree rule, sobatten checkdecided it underverify— which is what an agent runs repeatedly while iterating. Every fix commit moved the identity and owed another review, over bytes the deterministic gates had not judged yet.The keying was right; the enforcement point was wrong. So the module, its tier and its
MUTANT_GATESentry are deleted,patch read neveris withdrawn naming the successor, and the question is asked once at the event that spends CI minutes:A rebase-only lap resolves the same identity and buys nothing. A content change files the next review under a name the row does not look up.
And the waiver it needed exposed an inverted gate (CLOUD-1553)
Retiring the tree rule left
filed-over-own-diffrefusing rows this PR closes, with no walkable route on this host:closesneeds apr-closesrecord minted from agh pr viewcall and there is noghhere (CLOUD-1126, CLOUD-1481), and the class's override route binds acall:anchor nothing queries for a tree finding (CLOUD-1551).So the branch needs a
[[waiver]], and writing one surfaced a contradiction between the two halves of the waiver mechanism:waiver::applyconfig lintrule = "filed-over-own-diff"waiver-names-no-rulerule = "filed-here"A policy rule publishes PREDICATE ids and a finding carries the predicate id, so for any such rule no value satisfied both gates — and the spelling the lint blessed was the dead one, which is the "exemption someone is relying on" CLOUD-208 opened that smell to catch, produced by the catcher.
config lintalready receives the compiled bundles for its set analysis and simply never consulted them.Bundle::declaredis the module's own published set, so accepting a predicate id there acquires nothing and adds no second authority. Fixed here rather than filed, because a wrongly refusing gate is a defect to repair. The fix deliberately does not go the other way: makingwaiver::applymatch the rule id would let one waiver silently suppress every predicate a module publishes.The waiver therefore takes the narrow spelling, suppressing
filed-over-own-diffalone whilefiled-unrefinedstays live — which immediately earned its keep by catching two rows filed without Ready blocks.Three things this does NOT do, in
batten.tomlrather than discovered laterThe freeze invariant does not ship. A
push-needs-reviewrow is what would keep the readied head from changing under the ready gate — pricing to a transition is only sound if the head is frozen. It was written and measured, and it is too blunt to commit: areceiptrow selects on a command pattern, so it fires on every push. Measured over the suite, it deniedforced_push.rs's benign fixture push, and 15 test files drive a push unrelated to review. What it wants to refuse is a push to a branch whose PR is already ready, andforge.rscarries no draft field at all, so that narrowing is unsayable today. That is CLOUD-1446's gap and it closes in CLOUD-1548, where a declared loop can name the ready state. The tier keeps a push case over its own config, so the keying is shown not to be pattern-specific — but this repository is not frozen against a mid-ready push, and the header says so.Retiring the module widens the gate. It gated on
code-changedintersected with the reviewable prefixes, so a prose-only branch owed nothing. Areceiptrow has no such column, so a documentation-only branch now buys one review before readying. Deliberate and cheap at one dispatch per branch, but a widening rather than an oversight.The receipt attests the DISPATCH, not the completion. Measured on the live host: a skill arrives as
tool_name: "Skill"withtool_input: {"skill": "<name>"}, and its result is the bare stringLaunching skill: <name>. There is no field to require and nothing that says the agent then did what the skill told it.CI is honestly silent, not falsely green. The receipt store dies with the container, so until CLOUD-877 gives it a portable form this gate speaks where
landruns and abstains on a fresh checkout.Verification
Measured end to end on this branch, not asserted:
mise run test:cargo— 4958/4958 passed, 0 failed.crates/batten/tests/it/review_receipt_delta.rsover the compiled binary, driving realadjudicatecalls. The rebase and content-change cases are an anti-vacuity pair in the strict sense: aheadkeying fails the first and abranchkeying fails the second, so a suite carrying only one is silently satisfied by the wrong column.mise run policy-test— 58 bundles, 746 passed, 0 failed.batten config lint --config-from origin/main— 0, with both weakenings admitted (groomed).key_baseunclassified, the schema test caught the generated artifacts going stale, clippy's line ceiling caughtsmellsgrowing past 100, andcommit-lintcaught ten commits with noRefs:trailer. Every one is a gate doing its job, and none was hand-edited around.Filed rather than fixed here
Each was found by a review dispatched over this branch that reached past its own diff, and each is pre-existing: CLOUD-1551 (a tree finding's admission binds
call:and suppresses nothing), CLOUD-1552 (tasksetdrops non-literal argv words), CLOUD-1554 (the mediation bypass discards waiver records), CLOUD-1555 (singleton_task_startedanchors on the first argument).🤖 Generated with Claude Code
https://claude.ai/code/session_015H3zMrSGxu4K3fMZu4KFX1