Repository navigation
feat(ready-guard): refuse a hand-ready that does not hold the landing lease (CLOUD-420) - #368
Conversation
CLOUD-420 The landing lease is enforced only by the code path that honours it, so an agent that skips `land` still spends a full matrix
Why CLOUD-393 serialises landing behind a lease and cuts the discarded-CI-run rate. It is enforced entirely inside That is the failure this repository names on its front page: "A new rule without a runnable gate is half a change. Prose is feedforward only." The lease is a convention honoured by the cooperating path, and the threat model is the honest agent that does the wrong thing — CLOUD-200 records a session that satisfied The dominant case is residue, not defiance. Measured 05:17–05:19Z on 2026-08-12: four concurrent The enabling gap. The lease identifies a clone ( Refinement — Ready
Nothing in the table asks why a push happened, which is what makes it cover the residue case: the precondition is per job rather than per landing, so a push to a PR left ready by an interrupted The last row is the design and not a fallback: failing open costs one matrix, while failing closed on an unreadable ref stops every PR in the fleet, and a body minted before this change ages out within one TTL (120s).
Cost (§1), in the unit the invoice uses. This repository is private and every
Local execution is the unmetered tier and nothing here moves work onto the metered one. The CI-side check exists because the local one is the half an interrupted session never reaches. Test obligation
Commit / bump (§6): Blockers (§8): blockedBy CLOUD-363 — the stop conclusion is safe only once a cancelled required check reads as no verdict rather than as red, which is in flight on #302. CLOUD-393 landed in #340, so the lease exists, and adding Acceptance
CLOUD-393 Landing collisions cost ~2.9 CI runs per landed PR, and the recorded reason for not queueing answers a different question
Why
The measured cost of not queueing. A lap dies whenever The same memory argues this is correct — "collisions are designed for rather than prevented, in the CSMA/CD sense", and "a rising re-verify rate is NOT the stop signal" — and that argument is sound as far as it goes. What it does not do is compare against the alternative, because no alternative was costed. Two alternatives, both preserving exactly-tested-SHA fast-forward:
The honest test, and why this is Backlog rather than Todo. Refinement — Ready
Test obligation If a land lock ships: a lease that expires must be reclaimable, and a holder that dies must not wedge landing. Those two are the gate, and they are exactly the shape Commit / bump (§6): Blockers (§8): none formally, but this should not be pulled before CLOUD-386, CLOUD-390 and CLOUD-392 have landed and Acceptance
Raised from Low to Urgent: the number this issue said it lacked now existsThis issue argued the case for queueing and recorded that the honest test was to shorten the lap first and see whether the case survived. It did not survive. Measured 2026-08-11, 21:31–22:01Z (400
The bot is not the bottleneck — it answered every one of 248 attempts inside 23 seconds. 243 of them were genuine So the recorded position in Mechanism chosen: the land lock, the cheap half of the two options in the body. GitHub merge queue stays out of scope — it is a larger departure from Refinement — Ready
Test obligationTwo assertions the change rests on, both against a real remote rather than a fixture: two concurrent acquires — exactly one exits 0; and a holder whose lease was stolen reports not-held from its pre-comment re-check rather than proceeding. Plus a Commit / bump (§6): Blockers (§8): none. Acceptance
Implementation record — four things the Ready block got wrongAll four were found by testing against the real remote rather than by reasoning, and each is recorded because the wrong version is written down above. 1. The lease is a BRANCH, not
The lease is 2. Renewal is CAS, but not the CAS the plan claimed. I asserted 3. Delete goes through the REST API, not a push. The proxy 403s a delete-push too — the same reason 4. The lease body needs a nonce. Git addresses objects by content, so two mints agreeing on holder and expiry — the same clone renewing twice inside one second — produce the same sha, and pushing a sha the ref already points at is an "up to date" no-op that reports success. That turns a rejected claim into an apparent win. Measured before the fix: a second A defect in
|
| the lease | CAS-only, rolling TTL, no delete, no REST API |
land-lock-check |
the gate, because "stays well-formed" was prose |
| four reliability defects | FETCH_HEAD cross-read, heartbeat fragility, fence margin, clock skew |
| identity + delete refspec | commit-tree needs an identity; an empty mint was a delete |
| the test fix | label the two waits instead of deducing which is which |
the bare wait |
the one that mattered — see below |
The last one is the finding worth carrying forward. land's CI race ended with a bare wait, which blocks on every background job of the shell. This change adds one that by design never exits: the lease heartbeat. So from the moment the lease shipped, land would reach green CI and then block forever. It sat there for five minutes with all eight checks green and the SHA landable, logging nothing.
CLOUD-383 had already named the shape as an intermittent hang when a group kill misses. The heartbeat made it certain. The lease, as first written, could never have landed itself — and that was invisible to every test, because reproducing it requires a never-exiting child, which is precisely what would hang the suite. It is gated structurally now: no bare wait may appear in the task.
It also explains attempts I had attributed to contention and to a moving main. The last several were not that.
What is enforced, and what is not. The lease serialises landing across clones. It does not serialise within one — acquire is re-entrant per clone by design, so two land processes in one checkout both hold it (CLOUD-428, filed after exactly that happened here: three concurrent lands, two heartbeats). And it is honoured only by land itself; an agent that readies by hand still spends a matrix (CLOUD-420). Neither is a defect in what shipped; both are the boundary of it.
Not yet measured: the refusal:success ratio against the 243:5 baseline. The fleet went quiet at 22:01Z, so there is no comparable window yet. That measurement is this issue's acceptance criterion and remains outstanding — it should be taken before this moves to Done, not inferred from the fact that #340 landed in one lap.
CLOUD-418 A new gate is never shown to fail, so a test that cannot discriminate ships as coverage
Why
This repository's most-repeated failure is a claim nothing exercises. land's refusal branch was dead code for months (CLOUD-235). timeout-check's budgets were placeholders that could not fire (CLOUD-352). A shape rule whose pattern was a program could never match and read as coverage (CLOUD-401). Each was caught after the fact.
It happened again, live, while building the landing lease (CLOUD-393). A concurrency test was written for a real race — observe() reading FETCH_HEAD, which is one file per clone while the heartbeat runs beside held/release in the same checkout. The test was green. Then the buggy version was restored to check the test could catch it, and it passed on the broken code too: every process fetches the same lease ref, so a crossed read yields a different generation of the same lease rather than an observably foreign one. The test asserted nothing.
That was found only because someone chose to mutate and re-run — a discipline nothing asks for and nothing checks. The green suite before that check and the green suite after it were indistinguishable.
Root cause. The obligation is stated as "a rule ships with a runnable gate" — a gate that exists. Nothing requires evidence the gate discriminates. A test that passes on both the fixed and the broken code satisfies every rule this repo currently has.
Scope, deliberately narrow. Not mutation testing over the workspace, which is a research project and a large CI bill. The claim here is about mise-tasks/*-check and the guards — the files whose entire purpose is to refuse — where the mutation is usually a one-line inversion and the suite is bats, so a run is seconds.
Refinement — Ready
- Source of truth (§1). The gate's own suite, run against a deliberately broken copy of the gate. A pass there is the defect; the verdict is an exit code, not a judgement.
- Mechanism (§3). Undecided between two, and choosing is what Ready needs:
- Author-side, checked in. Each gate declares one or more
mutantcases — a stated one-line corruption and the test name that must go red. A task runs them, and a mutant nothing catches fails. Costs the repo one small fixture per gate; runs locally, off the landing path. - Scheduled sweep. A weekly job applies mechanical mutations to
mise-tasks/*-checkand reports any whose suite stays green. No per-gate authoring, weaker coverage, and it belongs besidebranch-age-checkin the hygiene sweep, so no new CI minutes.
- Author-side, checked in. Each gate declares one or more
- Deliberately not in scope (§2). Mutation coverage of
crates/. Different tooling, different cost, different question. - Output (§7). Pointer-only: the gate, the mutant, and the test that failed to notice. Never a diff of the mutated source.
Test obligation
The mechanism must catch the case that motivated it: the FETCH_HEAD mutation of mise-tasks/land-lock against tests/land-lock.bats as it stood before the structural assertion replaced it. That pair is a known-good fixture — a real gate, a real mutant, and a real suite that missed it.
Commit / bump (§6): feat(gate) — patch until 0.1.0 regardless of type.
Blockers (§8): none.
Acceptance
- Every
*-checktask has at least one mutation its suite is proven to catch. - A gate whose suite passes on a broken copy fails.
- The
land-lock/FETCH_HEADpair is covered as a regression fixture, so the case that motivated this cannot recur silently.
… lease CLOUD-420's other half, and the free one. The runner's precondition now stops an unauthorised run, but only after a matrix has been dispatched and cancelled; readying is the single event that starts CI, so refusing there costs nothing at all. Same predicate, applied to the fleet rather than to the commit: verify and linear-check say this commit is ready, and the lease says this branch is the one allowed to be spending CI on a landing right now. The receipt is written from `swap`, not from `acquire` as the refinement proposed, and that is the load-bearing choice. `swap` is the lease's only writer — acquire, renew, the heartbeat's steal path and release all reach the remote through it — so one insertion covers every way the lease can change hands and no caller can take it without leaving a receipt. Writing it in `acquire` instead puts it before the compare-and-swap is known to have won, so a REFUSED acquire leaves a receipt and ready-guard waves through precisely the clone the lease just refused: a gate that passes exactly when it should not. Mutation- checked rather than argued — moving the write into `acquire` reds `A REFUSED ACQUIRE LEAVES NO RECEIPT`. `swap` is also why the receipt survives a long lap. Every heartbeat renews the lease and therefore rewrites the receipt, so what is demanded is a lease that has not lapsed rather than one that was ever taken. An acquire-time receipt would have read as lapsed by the time it mattered: `verify` runs longer than a TTL, and `land` readies after its push (CLOUD-247), which is several minutes later. Keyed by BRANCH, like `claim-check`'s and unlike `verify`'s. A lease is a decision about which branch may land, and every commit on that branch continues to serve it; a sha-keyed one would demand a re-acquire per rebase, which is every lap. A case pins that another branch's receipt does not vouch for this one. A release REMOVES the receipt rather than letting it age out, since a release is a declaration that this clone no longer holds it and a lapsing receipt would keep honouring a lease already handed on. The key folds `/` to `-`, the transform claim-check and claim-guard already use on their own branch-keyed receipt, and the suites now check out a slashed branch because that is the only shape this repository produces. Written with the raw name first, and 68 green cases missed it: every branch here carries a slash (claude/…, wenzowski/…), which makes lease.claude/foo a path through a directory that never exists. The write fails, no receipt is left, and ready-guard then refuses every ready while looking exactly like a mechanism that works — a false refusal indistinguishable from the real one. A scratch repository's default branch is the one shape with no slash in it, which is precisely why the fixture hid it. Mutation-checked on the new fixture: the raw name reds three cases that passed under the old one. Fails open on a detached HEAD and on a receipt that is not a timestamp, matching every other unknown in the file. The asymmetry is affordable here in a way it is not on the runner: this half never has to guess, because the precondition is the backstop and it is the one that costs money. This does not gate `land`'s own ready, and does not need to. The hook sees the agent's own commands; `land` readies from inside `mise run land`, where no PreToolUse hook is watching, and it holds the lease by then anyway. What this refuses is the hand-ready — which is the shape that produced the residue in the first place, since `land` re-drafts only on red and a landing interrupted any other way leaves the PR ready permanently. Refs: CLOUD-420, CLOUD-393, CLOUD-418 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4tzyT3Q3hXo5QFEESENYP
…once had A bare `gh pr view` answers with the PR associated with this branch in ANY state, so once a branch name has carried a merged PR every later landing on that name binds the merged one. That is the default shape here rather than an edge case: trunk-based development deletes the branch on merge (CLOUD-349) while the session harness pins an agent to one branch name for its whole engagement, so the second landing of any session recycles a merged name. This branch carries eight PRs, seven of them merged. Observed rather than theorised: after #366 merged, a new commit and a new PR #368 on the same name produced `could not re-draft #366`. What kept that from being worse is incidental and worth naming, because it is the reason this is a fix and not a nicety — `redraft` runs before the wait loop and GitHub refuses to re-draft a merged PR, so the run died before reaching the terminal-state read, which treats MERGED as landed and exits 0. A bound-merged PR is one refactor away from reporting a landing that never happened, and a false completion signal is the one thing this repository exists to refuse. `gh pr list --head "$branch" --state open` instead, so "no open PR" falls through to the guard that already existed and names the fix. `branch` moves above the resolution because the query needs it. `// empty` rather than a bare field read: `--jq` prints the string "null" for a missing field, which is not empty and would sail past the guard as a PR number — `land` would then drive a pull request called "null" and block in the wait loop, which is a wedge rather than a failure. The stub had to learn to FILTER before either case could prove anything. First attempt returned the same body whether or not `--state open` was passed, so removing the flag left both cases green — a test that cannot see the defect it names. The real endpoint filters, so the stub now keeps two bodies: what an open-only query returns, and what an unfiltered one returns for a branch whose older PRs merged. Mutation-checked afterwards, which is the only reason this is stated as fact: dropping `--state open` reds both cases, and dropping `// empty` reds the no-open-PR case alone (with a non-empty list the fallback is not reachable, so the other case correctly stays green). Both resolution cases are bounded with `timeout`. The `// empty` mutant does not fail, it HANGS — `land` accepts "null" and waits forever — and an unbounded case wedges the whole file the way CLOUD-434's leaked stubs did. The bound turns a wedge into an ordinary failure the assertion can see. Refs: CLOUD-465, CLOUD-349, CLOUD-418 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4tzyT3Q3hXo5QFEESENYP
2570b23 to
9eb35b9
Compare
|
|
/fast-forward |
A merged PR is a completion signal for a DIFF. The board reads it as one for an ISSUE. Those coincide only when the issue has exactly one PR, and nothing checked that they do. Measured on the board itself, 2026-08-12. CLOUD-420 carries four pull requests — Todo -> In Progress 06:33 -> Done 07:17 -> In Progress 07:52: a premature Done, reversed by hand 35 minutes later. The same inference was then made independently by a session reading the same board. Two occurrences of one mechanizable error inside six hours, and two of CLOUD-420's own acceptance clauses were still unmet at the moment it read Done — both living in the PR nobody had noticed was open. That is the threat model rather than a novel class: honest error about a completion signal. Non-negotiable 2 says a rule without a runnable gate is half a change, so this is the gate. It is not the question its neighbours answer. CLOUD-192 is about WHEN the automation fires, merge rather than release; this is whether the transition is licensed at all given the issue's own attachments. `graph-check` owns the ready frontier and the In Review => linked PR rule, which is the entry to review rather than the exit from it. Neither can see a second PR still in flight. Arithmetic only: N attached PRs, k open => not Done. It never judges whether the merged ones did the work, which is not computable and would make this a judge rather than a gate. The PR filter is claim-check's, character for character, so the two agree by construction on what counts as a pull request. Two decisions worth stating. A missing PR state is exit 2, not a licence: the whole defect is a Done granted over a PR nobody checked, so an absent state must not be the cheapest route to that same outcome. And a closed-unmerged PR does NOT refuse — an abandoned or superseded PR is a decided outcome rather than work in flight, and refusing on it would block Done forever with no action that could clear it, which is the shape of a gate that gets bypassed rather than satisfied. Mutation-proven 5/5, including one genuine survivor: widening the PR filter to `pull/[0-9]+` passed every row, because the number capture already requires a leading slash and so rejects `how-to-pull/123` on its own. The host restriction was the half nothing exercised, so a forge link on another host would have read as a blocker. That row now exists. The harness mutates a COPY and verifies the tracked file's hash is unchanged on exit. An in-place harness makes a corrupted commit reachable from any concurrent `git add -A`, which staged a mutant into a pushed commit earlier the same day. CLOUD-420's real payload is a committed regression row, and driving the gate against it refuses with `CLOUD-420 open-pr (#368, draft)`. NAMED done-pr-check, not done-check as the issue's Mechanism clause says. That name landed first for a different predicate (CLOUD-192: no release tag contains a Done issue's commits) and is already wired into release-plz.yml. Merging the two was refused on evidence: that caller pipes Done rows carrying no .pulls, which this gate answers 2 for by design, so folding them together would turn a green release path red for a reason unrelated to releases. Registered in MUTANT_GATES, which the first draft omitted — the mutation run that proved this gate discriminates would not have run again without it. Refs: CLOUD-468
A merged PR is a completion signal for a DIFF. The board reads it as one for an ISSUE. Those coincide only when the issue has exactly one PR, and nothing checked that they do. Measured on the board itself, 2026-08-12. CLOUD-420 carries four pull requests — Todo -> In Progress 06:33 -> Done 07:17 -> In Progress 07:52: a premature Done, reversed by hand 35 minutes later. The same inference was then made independently by a session reading the same board. Two occurrences of one mechanizable error inside six hours, and two of CLOUD-420's own acceptance clauses were still unmet at the moment it read Done — both living in the PR nobody had noticed was open. That is the threat model rather than a novel class: honest error about a completion signal. Non-negotiable 2 says a rule without a runnable gate is half a change, so this is the gate. It is not the question its neighbours answer. CLOUD-192 is about WHEN the automation fires, merge rather than release; this is whether the transition is licensed at all given the issue's own attachments. `graph-check` owns the ready frontier and the In Review => linked PR rule, which is the entry to review rather than the exit from it. Neither can see a second PR still in flight. Arithmetic only: N attached PRs, k open => not Done. It never judges whether the merged ones did the work, which is not computable and would make this a judge rather than a gate. The PR filter is claim-check's, character for character, so the two agree by construction on what counts as a pull request. Two decisions worth stating. A missing PR state is exit 2, not a licence: the whole defect is a Done granted over a PR nobody checked, so an absent state must not be the cheapest route to that same outcome. And a closed-unmerged PR does NOT refuse — an abandoned or superseded PR is a decided outcome rather than work in flight, and refusing on it would block Done forever with no action that could clear it, which is the shape of a gate that gets bypassed rather than satisfied. Mutation-proven 5/5, including one genuine survivor: widening the PR filter to `pull/[0-9]+` passed every row, because the number capture already requires a leading slash and so rejects `how-to-pull/123` on its own. The host restriction was the half nothing exercised, so a forge link on another host would have read as a blocker. That row now exists. The harness mutates a COPY and verifies the tracked file's hash is unchanged on exit. An in-place harness makes a corrupted commit reachable from any concurrent `git add -A`, which staged a mutant into a pushed commit earlier the same day. CLOUD-420's real payload is a committed regression row, and driving the gate against it refuses with `CLOUD-420 open-pr (#368, draft)`. NAMED done-pr-check, not done-check as the issue's Mechanism clause says. That name landed first for a different predicate (CLOUD-192: no release tag contains a Done issue's commits) and is already wired into release-plz.yml. Merging the two was refused on evidence: that caller pipes Done rows carrying no .pulls, which this gate answers 2 for by design, so folding them together would turn a green release path red for a reason unrelated to releases. Registered in MUTANT_GATES, which the first draft omitted — the mutation run that proved this gate discriminates would not have run again without it. Refs: CLOUD-468
A merged PR is a completion signal for a DIFF. The board reads it as one for an ISSUE. Those coincide only when the issue has exactly one PR, and nothing checked that they do. Measured on the board itself, 2026-08-12. CLOUD-420 carries four pull requests — Todo -> In Progress 06:33 -> Done 07:17 -> In Progress 07:52: a premature Done, reversed by hand 35 minutes later. The same inference was then made independently by a session reading the same board. Two occurrences of one mechanizable error inside six hours, and two of CLOUD-420's own acceptance clauses were still unmet at the moment it read Done — both living in the PR nobody had noticed was open. That is the threat model rather than a novel class: honest error about a completion signal. Non-negotiable 2 says a rule without a runnable gate is half a change, so this is the gate. It is not the question its neighbours answer. CLOUD-192 is about WHEN the automation fires, merge rather than release; this is whether the transition is licensed at all given the issue's own attachments. `graph-check` owns the ready frontier and the In Review => linked PR rule, which is the entry to review rather than the exit from it. Neither can see a second PR still in flight. Arithmetic only: N attached PRs, k open => not Done. It never judges whether the merged ones did the work, which is not computable and would make this a judge rather than a gate. The PR filter is claim-check's, character for character, so the two agree by construction on what counts as a pull request. Two decisions worth stating. A missing PR state is exit 2, not a licence: the whole defect is a Done granted over a PR nobody checked, so an absent state must not be the cheapest route to that same outcome. And a closed-unmerged PR does NOT refuse — an abandoned or superseded PR is a decided outcome rather than work in flight, and refusing on it would block Done forever with no action that could clear it, which is the shape of a gate that gets bypassed rather than satisfied. Mutation-proven 5/5, including one genuine survivor: widening the PR filter to `pull/[0-9]+` passed every row, because the number capture already requires a leading slash and so rejects `how-to-pull/123` on its own. The host restriction was the half nothing exercised, so a forge link on another host would have read as a blocker. That row now exists. The harness mutates a COPY and verifies the tracked file's hash is unchanged on exit. An in-place harness makes a corrupted commit reachable from any concurrent `git add -A`, which staged a mutant into a pushed commit earlier the same day. CLOUD-420's real payload is a committed regression row, and driving the gate against it refuses with `CLOUD-420 open-pr (#368, draft)`. NAMED done-pr-check, not done-check as the issue's Mechanism clause says. That name landed first for a different predicate (CLOUD-192: no release tag contains a Done issue's commits) and is already wired into release-plz.yml. Merging the two was refused on evidence: that caller pipes Done rows carrying no .pulls, which this gate answers 2 for by design, so folding them together would turn a green release path red for a reason unrelated to releases. Registered in MUTANT_GATES, which the first draft omitted — the mutation run that proved this gate discriminates would not have run again without it. Refs: CLOUD-468



CLOUD-420's other half, and the free one. The runner-side precondition (landed in
dcead67) stops an unauthorised run — but only after a matrix has been dispatched and cancelled. Readying is the single event that starts CI, so refusing there costs nothing at all.Same predicate as the two receipts already required, applied to the fleet rather than to the commit:
verifyandlinear-checksay this commit is ready; the lease says this branch is the one allowed to be spending CI on a landing right now.Why the receipt is written from
swap, notacquireswapis the lease's only writer — acquire, renew, the heartbeat's steal path and release all reach the remote through it — so one insertion covers every way the lease can change hands, and no caller can take it without leaving a receipt.Writing it in
acquire, as the refinement specified, puts it before the compare-and-swap is known to have won. A refused acquire would then leave a receipt, andready-guardwould wave through precisely the clone the lease just refused: a gate that passes exactly when it should not. Mutation-checked rather than argued — moving the write intoacquireredsA REFUSED ACQUIRE LEAVES NO RECEIPT.swapis also what makes the receipt survive a long lap. Every heartbeat renews the lease and so rewrites the receipt, meaning what is demanded is a lease that has not lapsed, rather than one that was ever taken. An acquire-time receipt would read as lapsed by the time it mattered:verifyruns longer than a TTL, andlandreadies after its push (CLOUD-247).The branch key, and the bug 68 green cases missed
The key folds
/to-— the transformclaim-checkandclaim-guardalready use on their own branch-keyed receipt.Written with the raw name first. Every branch in this repository carries a slash (
claude/…,wenzowski/…), which makeslease.claude/fooa path through a directory that never exists: the write fails, no receipt is left, andready-guardthen refuses every ready while looking exactly like a mechanism that works. A scratch repository's default branch is the one shape with no slash in it, which is precisely why the fixture hid it. Both suites now check out a slashed branch, and mutation-checking on the new fixture reds three cases that passed under the old one.Scope
Keyed by branch, like
claim-check's and unlikeverify's: a lease is a decision about which branch may land, and every commit on it continues to serve that. A sha-keyed receipt would demand a re-acquire per rebase, which is every lap. A release removes the receipt rather than letting it age out, since a release is a declaration that this clone no longer holds it.Fails open on a detached HEAD and on a receipt that is not a timestamp, matching every other unknown in the file. The asymmetry is affordable here in a way it is not on the runner: this half never has to guess, because the precondition is the backstop and it is the one that costs money.
This does not gate
land's own ready and does not need to — the hook sees the agent's own commands, whilelandreadies from insidemise run landwhere noPreToolUsehook is watching, and it holds the lease by then anyway. What this refuses is the hand-ready, which is the shape that produced the residue:landre-drafts only on red, so a landing interrupted any other way leaves the PR ready permanently.Verification
68 cases across
tests/land-lock.batsandtests/ready-guard.bats. Four mutants, each confirmed discriminating:swapwrites no receiptready-guardnever reaches the lease predicateacquire(the refinement's design)A REFUSED ACQUIRE LEAVES NO RECEIPTRefs: CLOUD-420, CLOUD-393, CLOUD-418
Generated by Claude Code