Skip to content

feat(ready-guard): refuse a hand-ready that does not hold the landing lease (CLOUD-420) - #368

Merged
wenzowski merged 2 commits into
mainfrom
claude/landing-lease-optimization-hkmacd
Aug 12, 2026
Merged

wenzowski merged 2 commits into
mainfrom
claude/landing-lease-optimization-hkmacd

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

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: verify and linear-check say 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, not acquire

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, as the refinement specified, puts it before the compare-and-swap is known to have won. A refused acquire would then leave a receipt, and ready-guard would 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 into acquire reds A REFUSED ACQUIRE LEAVES NO RECEIPT.

swap is 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: verify runs longer than a TTL, and land readies after its push (CLOUD-247).

The branch key, and the bug 68 green cases missed

The key folds / to - — the transform claim-check and claim-guard already use on their own branch-keyed receipt.

Written with the raw name first. Every branch in this repository 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 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 unlike verify'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, while 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: land re-drafts only on red, so a landing interrupted any other way leaves the PR ready permanently.

Verification

68 cases across tests/land-lock.bats and tests/ready-guard.bats. Four mutants, each confirmed discriminating:

mutant reds
swap writes no receipt the 3 writer cases
ready-guard never reaches the lease predicate exactly the 4 lease cases; the allow cases stay green
the receipt is written by acquire (the refinement's design) A REFUSED ACQUIRE LEAVES NO RECEIPT
the raw branch name the 3 writer cases, on the slashed fixture only

Refs: CLOUD-420, CLOUD-393, CLOUD-418


Generated by Claude Code

@linear-code

linear-code Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
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 mise-tasks/land. An agent that runs gh pr ready by hand, pushes to an already-ready PR, or wraps landing in its own logic spends a full four-job matrix without ever touching the lock.

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 gh-guard and then wrapped mise run land in a bespoke retry loop it invented on the spot. Nothing stops the same shape here.

The dominant case is residue, not defiance. Measured 05:17–05:19Z on 2026-08-12: four concurrent pull_request matrices (#356, #354, #332, #302) while the lease changed hands three times, so the lease was being honoured by every session that held it and all four matrices ran anyway. land re-drafts only on red, so a landing interrupted any other way — a container reclaim, a stopped task, a lost lease, a die on a rebase conflict — leaves the PR ready permanently, and every later push to it buys a full matrix with no landing attempt in progress at all. All three of the other PRs were draft=false. The implementation is scoped to that case; an agent that skips land deliberately is the same mechanism and the smaller share.

The enabling gap. The lease identifies a clone (hostname-pid-random), which CI cannot check anything against. Adding the branch it authorises to the lease body is what makes it verifiable by the thing spending the money.

Refinement — Ready

  • Source of truth (§1). The lease ref refs/heads/batten-land-lock — a parentless commit over the empty tree whose body is land-lock / holder: / expires: / nonce: — read server-side by the workflow that is about to spend the runner. This issue adds a fifth line, branch:, and land-lock-check gains its well-formedness assertion rather than a second gate growing beside it. Not a receipt in a clone, which is exactly the artefact an agent skipping land never writes.

  • Mechanism (§3). land-lock gains a read-only verb, authorises <branch> — a pure function of (lease state, branch) reusing the existing observe / expired / released predicates. The alternative, re-parsing the body in workflow YAML, is a second authority for the ref and silently drops the read semantics the lease was pressure-tested into: a per-process observation ref, an unparseable body reading as held, and an unreachable remote that is not "free". Exit 0 run / 3 stop / 2 could not look.

    Each expensive pull_request job gains a first step, before the real checkout: a one-file sparse checkout of mise-tasks/land-lock at main (fetch-depth: 1, ~2s) and one invocation of it. The predicate comes from trunk, never from the head under test — that head has not been graded yet.

lease state action
absent, released, or expired-and-corroborated run — nobody is landing
branch: is this branch run — this is the authorised attempt
branch: names another branch stop, having spent only the spin-up
unreachable, unparseable, or carrying no branch: run, with a warning — fail open

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 land is stopped by the same row that stops a hand-typed gh pr ready.

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).

  • A step, not a preflight job (§3), and the arithmetic that decides it. In billed minutes the two are closer than a wall-clock reading suggests: a gate job costs 1 job-minute per stopped run, because the expensive jobs never start and a skipped job bills nothing, while a first step in each job costs 7. Against that, the gate taxes every legitimate run one extra job-minute plus ~15-20s of serial spin-up. The step is therefore cheaper exactly while legitimate runs outnumber stopped ones better than 6:1, and lap length sits in the exponent of the collision model this whole line of work exists to shrink, which decides it at parity. That ratio is measurable rather than assumed; it is the number to re-check if the residue rate stays high.

  • THE HAZARD — a stop is a third answer, and the verdict layer has two (§2). A first step cannot make a job skipped; only a job-level if: can, which is the preflight job ruled out above. So the reachable conclusions are success, failure and cancelled, and checks-green reads success or neutral as green, failure and cancelled as red, and a latest-run skip as "not an answer".

    • Success or neutral grades a head green that compiled nothing, and main advances to exactly the SHA that passed. Disqualifying.
    • Failure re-drafts healthy PRs fleet-wide, since land re-drafts on red. Disqualifying.
    • Cancelled is the one that can be made correct. "This run declined to answer" is a third thing the verdict vocabulary lacks, and CLOUD-436's latest-run-per-name grouping is what makes a per-name declination readable at all.

    So: the step self-cancels the run (POST /actions/runs/{id}/cancel, actions: write on that job alone). Reading cancelled as a declination rather than as red is CLOUD-363's change, in flight on fix(tasks): read a cancelled required check as no verdict, not as red #302, which moves it into skipped's not-an-answer bucket in checks-green and in land's graded_runs together. This issue consumes that predicate rather than restating it, and that is why it is a blocker rather than a note: a stop landing first would re-draft healthy PRs fleet-wide, which is the hazard this bullet exists to avoid.

    Two consequences, both load-bearing:

    • Branch protection requires only final, which concludes cancelled too, so an unauthorised head cannot be merged while it waits.
    • Because graded_runs reads a cancelled set as ungraded, the stopped head's next land lap re-fires its ready and mints a real run, so the re-dispatch this would otherwise have to specify arrives with CLOUD-363's second half. It cannot loop: land acquires the lease before it pushes, so a land-driven push is never the one stopped.
  • Humans are unaffected by construction (§2). The "absent or released" row is what preserves them: a person pushing while no agent holds the lease sees no change. They wait only in the case where their run would have been voided anyway, which is a saving rather than a tax. fast-forward.yml is not touched.

  • Below it, for free (§3). Extend ready-guard — which already denies gh pr ready without verify and linear-check receipts — to require a lease.<branch> receipt, written by land-lock acquire into the same receipts directory. Catches the honest mistake at zero CI cost. A fail-fast convenience, not the guarantee: a hook can be unloaded (CLOUD-187) or bypassed, which is the whole reason the server-side check is the load-bearing half.

  • Above it (§3). A ci-local-parity property asserting that every pull_request job which checks the repository out carries the precondition as its first step, so a new job cannot silently omit it. The exemption is computable rather than enumerated: a job with no actions/checkout — final, the fan-in — has no work to protect.

  • Engine gap, per §2 (§3). That property is a predicate over workflow structure, which no rule kind can address, so it extends ci-local-parity instead of landing in batten.toml. The gap is CLOUD-452, linked rather than left as an exception.

Cost (§1), in the unit the invoice uses. This repository is private and every pull_request job is a GitHub-hosted ubuntu-latest runner, so the metered unit is a job-minute rounded up per job, not a second of wall clock. Measured p95 per job, 2026-08-12: ci 429s, darwin-link 97s, cross 82s, commit-lint 65s, msrv 60s, final 14s, action 12s — ~759s of runner time, which bills as 17 job-minutes per full run.

  • The tax on a legitimate run is ~2s in each of 7 jobs, which per-job rounding absorbs: 0 billed minutes, unless a job already sits within 2s of a minute boundary.
  • A stopped run still bills its floor — 7 jobs x 1 minute = 7 job-minutes against the 17 it replaces. That is ~59% of the invoice and ~86% of the wall clock: real, and smaller than a seconds-based reading implies.
  • The cheapest option for the residue case spends nothing at all, because a push to a PR left draft triggers no job: CLOUD-458. This issue is the backstop for what still leaks past that, so it must not be sized as though it carried the volume.

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

  • tests/land-lock.bats — the four rows above as a pure function of (lease body, branch), plus the two that must not be conflated: unreachable is not absent, and a missing branch: is not a foreign holder.
  • tests/land.bats — the composition rather than the predicate, which CLOUD-363 owns and tests: a head whose required runs were stopped by the precondition reads as ungraded, so the next lap re-fires the ready instead of re-drafting. Mutation-checked per CLOUD-418: with the stop step removed from the fixture workflow, that head grades normally.
  • tests/ci-local-parity.bats — both directions: a checkout-bearing pull_request job without the precondition is refused, and a job with no checkout is not.
  • tests/land-lock-check.bats — a lease body with no branch: line is reported.

Commit / bump (§6): feat(ci) — patch until 0.1.0 regardless of type.

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 branch: to its body is this issue's own first step; CLOUD-436 landed in #347, supplying the latest-run-per-name grouping the declination sits on.

Acceptance

  • A PR pushed while another branch holds the lease stops within seconds instead of running a matrix.
  • A PR pushed while the lease is free, or while its own branch holds it, runs unchanged.
  • A stopped run neither reds the PR nor leaves its head unrecoverable: the branch's own next lap mints a real run without a hand-minted SHA.
  • A push to a PR left ready by an interrupted land is stopped by the same row as a hand-typed ready, with no landing attempt in progress.
  • An unreachable lease, or one carrying no branch:, runs the matrix rather than stopping the fleet.
  • A new checkout-bearing pull_request job cannot omit the precondition without failing ci-local-parity.
  • land-lock-check fails a lease body missing branch:.

CLOUD-393 Landing collisions cost ~2.9 CI runs per landed PR, and the recorded reason for not queueing answers a different question

Why

mem:workflow/agent-fanout records the position: "There is deliberately no cap on In Review: in a fast-forward trunk nothing queues there." That sentence is true and it is about the board column — how many landed-but-unreleased issues may sit in In Review. It is routinely read as also settling a different question: whether landing itself should be serialised. It does not settle that, and this issue exists so the two are not conflated again.

The measured cost of not queueing. A lap dies whenever main moves under it. With mean inter-land gap G = 487s and lap length L, a lap survives with probability ≈ e^(-L/G), so expected CI runs per landed PR is e^(L/G) — ~2.9 at today's L, matching the observed 8, 3, 4 and 2 laps. Roughly two of every three CI runs on the landing path are discarded work.

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:

  • A land lock. An advisory lease — a ref like refs/land-lock, or a label — taken before ready+push and released on merge or on a timeout. The holder gets an uncontended lap; everyone else waits, and waiting is free. This converts a collision (one wasted CI run) into a queue (no CI run at all). It changes nothing about CI, nothing about the fast-forward, and needs no host feature. The risk is the classic one: a lease whose holder dies must expire, or landing stops for everyone — so the expiry is the whole design, not a detail.
  • GitHub merge queue. It builds a merge_group ref, runs CI on it, and lands that exact tested SHA linearly — so the "main takes the PR's exact, already-passed commits" invariant survives. It also batches N PRs into one CI run, which at this land rate would cut the bill further rather than raise it. The real costs are honest ones: a second required-check surface (merge_group must be a CI trigger), and a genuine departure from /fast-forward-by-comment as repository discipline — which batten.toml deliberately does not encode as a host constraint.

The honest test, and why this is Backlog rather than Todo. L is in the exponent, so shortening the lap raises p on its own. CLOUD-386 has already taken the shell suite from 248s to 72.73s; CLOUD-390 and CLOUD-392 would take more out of L. Re-measure p after those land before spending anything here. If p rises far enough, the recorded position is vindicated by measurement rather than by assertion, and this issue should be closed as declined — with the number written down. If it does not, the comparison above is what the decision needs.

Refinement — Ready

  • Source of truth (§1). Observed laps-per-landed-PR from land's own runs and the workflow run history, before and after the L-shortening issues land. Not a number a human types, and not a model's estimate.
  • Mechanism (§3). None proposed yet — deliberately. This issue's deliverable at Ready time is the re-measured p, and a decision recorded against it. Promoting it to Todo requires that number to exist.
  • Deliberately not in scope (§2). Anything that changes what CI proves or how main advances. Both options above are about who goes first, never about what is verified.
  • The invariant that must not move. main advances only to a SHA that CI graded green, by fast-forward. Any queueing design that re-writes commits after testing them — or that lands a merge commit — fails this and is out of scope regardless of its speed.

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 mise-tasks/target-ensure already solved for the rustup lock (CLOUD-220) — reuse that reasoning rather than reinventing it.

Commit / bump (§6): feat(land) — patch until 0.1.0 regardless of type.

Blockers (§8): none formally, but this should not be pulled before CLOUD-386, CLOUD-390 and CLOUD-392 have landed and p has been re-measured.

Acceptance

  • Laps-per-landed-PR is re-measured after the L-shortening work, and recorded.
  • The decision — queue, land-lock, or decline — is written against that number rather than against either the original CSMA/CD argument or this issue's counter-argument.
  • If declined, mem:workflow/agent-fanout gains the measured p so the question is settled by data next time it is raised.

Raised from Low to Urgent: the number this issue said it lacked now exists

This 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 fast-forward.yml runs, 248 executed — full method and caveats in CLOUD-399):

quantity value
landing attempts 248 in 30 minutes
merges 5
per-attempt success rate ~2%
bot answer time median 12s, max 23s

The bot is not the bottleneck — it answered every one of 248 attempts inside 23 seconds. 243 of them were genuine sequoia-pgp/fast-forward refusals: the branch was behind by the time it asked. This is a thundering herd, and it is the exact cost of not queueing, paid in CI minutes instead of in waiting.

So the recorded position in mem:workflow/agent-fanout — "in a fast-forward trunk nothing queues there" — is falsified, not merely arguable. The queue exists today; it is just implemented as 243 wasted CI matrices per half hour instead of as a lease. Amending the memory in place is part of this change rather than a follow-up.

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 /fast-forward-by-comment as repository discipline, and the lease answers the measured problem without a host feature.

Refinement — Ready

  • Source of truth (§1). fast-forward.yml run history: the refusal:success ratio over a window, before and after. Not an estimate, and re-sampled rather than recalled.
  • Mechanism (§3). A lease on refs/land-lock at the origin. Claim is an atomic server-side test-and-set — a create-only push is rejected as non-fast-forward if the ref exists, so two sessions cannot both hold it. Release is a lease-guarded delete, so it cannot delete a lease someone else has since taken. Held from just before the push that starts CI through the merge, so the only CI matrix running for a landing attempt is the holder's. Deliberately not held across local verify — that is ~150s touching no shared resource.
  • Liveness (§3). Expiry as p95 × 3 in the grammar timeout-check already defines, refreshed by a heartbeat while the holder genuinely progresses, so the TTL can be short without expiring healthy holds. A holder re-checks the lease immediately before commenting /fast-forward — the cheap stand-in for a fencing token, and what makes a stolen lease safe rather than a double-comment.
  • Deliberately not in scope (§2). FIFO ordering — v1 is lease-plus-jittered-backoff, the CSMA/CD posture agent-fanout already argues for. Also excluded, and each for a stated reason: synthesising a red check on non-holders (red must mean broken, and land re-drafts on red, so it would re-draft healthy PRs fleet-wide); cancelling other PRs' in-flight runs (can cancel the legitimate holder, needs cross-PR permissions, and reverses CLOUD-240's deliberate "supersede your own runs, never cancel someone else's"); auto-redding a timed-out holder (it was slow, not broken — re-draft is the honest signal and already exists); and penalising a PR that goes red while holding (every mature queue evicts the failing change and proceeds rather than carrying a grudge; the lost turn is the penalty).
  • Output (§7). Pointer-only: holder ref and lease age, never a payload.

Test obligation

Two 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 land case per exit path proving the lock is never leaked, including the rebase-conflict stop and the red-CI re-draft.

Commit / bump (§6): feat(land) — patch until 0.1.0 regardless of type.

Blockers (§8): none. relatedTo CLOUD-399, whose candidate #1 this implements.

Acceptance

  • Two concurrent acquires: exactly one wins, proven against the real origin.
  • The lock is released on every exit path, proven per path rather than by inspection.
  • Re-sampled refusal:success ratio is materially better than the 243:5 baseline — and reported, not assumed.
  • CI runs per landed PR trends toward 1.
  • checks-green and the final fan-in are untouched: the green predicate does not change.

Implementation record — four things the Ready block got wrong

All 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 refs/land-lock. Two independent reasons, both measured:

  • The agent proxy answers 403 to a push at any ref outside refs/heads — so a custom namespace is unwritable from a session at all.
  • GitHub does not enforce the fast-forward rule off refs/heads. A PATCH of a parentless orphan with force: false was accepted on refs/batten/land-lock; the same request against a branch returned 422 Update is not a fast forward. So the atomicity the design depends on only exists on refs/heads.

The lease is refs/heads/batten-land-lock. branch-age-check needs no exemption: a lease lives minutes and heads no merged PR, and a leaked one showing up as stale is the gate working.

2. Renewal is CAS, but not the CAS the plan claimed. I asserted PATCH ... force:false would give compare-and-swap via the fast-forward rule. Measured: it does not, per (1). What does work is git push --force-with-lease=<ref>:<observed> — the correct expected value wins, a stale one is rejected with stale info. That is a true CAS and it is what renew, the heartbeat and the steal all use. Create stays a plain push (rejected as non-fast-forward if the ref exists), so acquire is still an atomic test-and-set.

3. Delete goes through the REST API, not a push. The proxy 403s a delete-push too — the same reason land's post-merge branch delete already fails and is tolerated. So release calls gh api -X DELETE. That asymmetry is environmental, not a design choice, and it is why every caller checks mine first: there is no lease guard available on that path.

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 acquire from the holder reported "acquired" rather than recognising its own lease, and a renew left the ref unmoved.

A defect in land that the lease exposed

land's CI race used a bare wait -n, which returns on the first job of any kind to exit. The lease heartbeat is also a job of that shell, so a heartbeat that ends — its lease lost, or a stub returning at once — was read as the race concluding, leaving both result files empty and every lap reporting "no verdict". It turned all of tests/land.bats into no-verdict laps. Fixed by naming the two contenders: wait -n "$ci_pid" "$main_pid". The bug was latent before this change and would have fired the first time any future background job was added.

Rolling TTL, and a second backstop

The TTL is 120s with a 30s heartbeat, not the static p95 x3 = 1030s the Ready block above specifies. A static TTL has to bound how long a hold might legitimately take — a guess, wrong in both directions: too low and a slow CI run is stolen from mid-flight, too high and a reclaimed VM blocks the fleet for the whole window. A rolling lease only has to bound how long until we notice a holder stopped beating, which is small, stable and independent of CI duration. Three missed beats before declaring death is the usual Raft/etcd margin. A dead holder now blocks landing for ~2 minutes rather than ~17.

A lap that loses the lease spends no CI, so it does not count against LAND_MAX_LAPS — that backstop exists to catch "main is moving faster than a lap takes", and counting waits would make a busy fleet exhaust it without ever attempting. But "never counted" is how a loop becomes unbounded with nothing that can fire, so waits get their own: LAND_LOCK_MAX_WAITS (8) consecutive whole waits lost reports the fleet saturated. Not a wall clock on a poll — each wait already ran its full duration — a count of turns lost, which is a real and diagnosable state.


Landed — #340, 0552c41, in one lap

Seven commits. Four of them fix defects in the lease itself, all found after it was first "done", and three of those were found by a gate refusing rather than by re-reading the design:

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 mutant cases — 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/*-check and reports any whose suite stays green. No per-gate authoring, weaker coverage, and it belongs beside branch-age-check in the hygiene sweep, so no new CI minutes.
  • 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 *-check task 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_HEAD pair is covered as a regression fixture, so the case that motivated this cannot recur silently.

Review in Linear

claude added 2 commits August 12, 2026 13:17
… 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
@wenzowski
wenzowski marked this pull request as ready for review August 12, 2026 13:22
@wenzowski
wenzowski force-pushed the claude/landing-lease-optimization-hkmacd branch from 2570b23 to 9eb35b9 Compare August 12, 2026 13:22
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 9eb35b9 into main Aug 12, 2026
8 checks passed
@wenzowski
wenzowski deleted the claude/landing-lease-optimization-hkmacd branch August 12, 2026 13:29
wenzowski pushed a commit that referenced this pull request Aug 13, 2026
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
wenzowski pushed a commit that referenced this pull request Aug 13, 2026
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
wenzowski pushed a commit that referenced this pull request Aug 13, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants