Skip to content

fix(land): close the tap on every non-merged exit, not only on red (CLOUD-458) - #362

Merged
wenzowski merged 2 commits into
mainfrom
claude/groom-cloud-420-1bbuf0
Aug 12, 2026
Merged

wenzowski merged 2 commits into
mainfrom
claude/groom-cloud-420-1bbuf0

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Only the red path re-drafted, so a landing interrupted any other way — a lost
lease, a rebase conflict, a stopped task — left the PR ready for good, and every
later push to it bought a full matrix with no landing attempt in progress at all.
Measured 2026-08-12, 05:17–05:19Z: four concurrent pull_request matrices while
the lease changed hands three times, three of the four PRs draft=false behind
an interrupted land.

The re-draft is conditional, and that is the design

The pre-push ready fires only on a head with no graded run, so re-drafting a
head that already graded green strands it: unmergeable while draft, unreadyable
because graded_runs is no longer zero. Readying it anyway is the worse repair —
ready_for_review is a CI trigger, so recovering a green head buys a whole
matrix where that resume is a free fast-forward today.

head at exit action
ungraded, or graded not-green re-draft
graded green leave ready
could not read the verdict leave ready — never strand a head on a reading we failed to take
merged leave alone

checks-green is the authority for "is this head green", not graded_runs: the
two answer different questions, and the one that counts a failed or cancelled run
as an answer would leave a green head ready only by accident.

What a reviewer should look at

  • close_the_tap runs first in the EXIT trap and is guarded on singleton_held,
    so a refused second land — which owns neither the lease nor the PR — touches
    nothing. Same discipline the lease release already uses.
  • It asks only for isDraft. Whether the PR is still open is already answered by
    landed and by the die above, and pr view --json state is sequenced by the
    poll it belongs to.
  • A SIGKILLed land runs no trap, so this closes the graceful subset only. The
    server-side precondition (CLOUD-420) is what catches the rest.

Tests

tests/land.bats gains six cases, mutation-checked in both directions: remove the
trap call and the ungraded case goes back to leaving the PR ready; remove the
green guard and the green case starts re-drafting. Plus the unreadable verdict,
the merged path (with the verdict lever set to the value that would re-draft, so
it asserts the flag rather than the absence of an opportunity), the refused
singleton, and a failed re-draft not changing the exit code.

Refs: CLOUD-458

@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-458 `land` re-drafts only on red, so every other interrupted landing leaves a ready PR that buys a full matrix on any later push

Why

land readies a PR and re-drafts it on exactly one exit: red CI. Every other way a landing ends — a container reclaim, a stopped background task, a lost lease, a die on a rebase conflict — leaves the PR ready permanently. A ready PR is a standing invitation: every later push to it buys a full four-job matrix with no landing attempt in progress at all.

Measured 2026-08-12, 05:17–05:19Z: four concurrent pull_request matrices on four branches (#356, #354, #332, #302) while the landing lease changed hands three times. The lease was being honoured by every session that held it. Three of the four PRs were draft=false, and this repository produced several such PRs the same evening — #342, #343 and #345 each survived an interrupted land while ready.

The draft economy is the whole reason drafts exist here: CI does not run on drafts, so iteration is free until a landing is actually being attempted. Readying is how land says "I am landing this now". Nothing says the opposite when it stops.

Refinement — Ready

  • Source of truth (§1). The PR's draft state, which land already owns: it is the only writer of gh pr ready in this repository and already re-drafts on the red path. This adds no new state and no receipt — the invariant is that land leaves the PR draft unless it landed it.
  • Mechanism (§3). land's existing on_exit trap — which already reaps the race watchers and drops the lease on every exit path — re-drafts the PR when the landing did not merge. The predicate is a property the trap can already read: a PR that exists, is open, and was not fast-forwarded by this run. No new lifecycle step, no new task.
  • The re-draft is conditional on the head's verdict (§2), and an unconditional one costs a matrix. land's pre-push ready fires only when the head carries no graded run and the PR is a draft. So an unconditional re-draft strands a head that already graded green: graded_runs is non-zero, the ready never re-fires, and the PR is unmergeable and unreadyable at once. Readying it anyway is the worse repair — ready_for_review is a CI trigger, so recovering a green head would buy a full 17-job-minute matrix where today the resume is a free fast-forward. The predicate is therefore:
head at exit action why
ungraded, or graded not-green re-draft any resume needs a fresh run regardless, so the tap closes at no cost
graded green leave ready the resume is a free fast-forward; re-drafting would buy a matrix to get back to where it already is
merged leave alone it landed
  • What still leaks (§2). A stray iteration push onto a green head left ready. That one is CLOUD-420's row rather than this one's, and it is why these are companions.
  • This closes the graceful subset only (§2). A container reclaim SIGKILLs the process, so no trap runs and the PR stays ready. That bound is why this is a companion to CLOUD-420's server-side precondition rather than a substitute for it: the precondition is checked per job by the thing spending the money and does not depend on any local process getting a chance to clean up. This one closes the tap; that one catches what still leaks.
  • Effect (§3). write — it changes a PR's state. Already the class land operates in; no new command surface.
  • Output & exit contract (§5). Unchanged. The re-draft is a side effect of an exit that already has its own status; it must never change the exit code, and a failed re-draft is reported and swallowed exactly as the lease drop is — an exit path that can fail on cleanup is worse than the leak it fixes.

Test obligation

tests/land.bats gains the exit matrix: a die on a rebase conflict over an ungraded head leaves the PR draft, and the same die over a green head leaves it ready; a lost lease and a killed watcher follow those same two rows; a run that lands leaves the PR merged; a refused second land touches nothing, since it owns neither the lease nor the PR; a failed re-draft does not change the exit code. Mutation-checked in both directions per CLOUD-418: with the re-draft removed from the trap the ungraded fixture goes back to leaving the PR ready, and with the green-head guard removed the green fixture starts re-drafting.

Commit / bump (§6): fix(land) — patch until 0.1.0.

Blockers (§8): none.

Acceptance

  • A landing that ends any way other than merging leaves its PR draft, unless the head already graded green.
  • A green head left ready by an interrupted landing is still landable by re-running land alone, with no new CI run.
  • A failed re-draft never changes land's exit code.
  • A refused second land in the same clone leaves the live one's PR untouched.
  • The SIGKILL case is stated as out of reach rather than claimed as covered.

Review in Linear

claude added 2 commits August 12, 2026 07:03
`land` readies a PR and re-drafted it on exactly one exit: red CI. Every
other way a landing ends — a lost lease, a rebase conflict, a stopped task,
a container reclaim — left the PR ready for good, and every later push to it
bought a full matrix with no landing attempt in progress at all. Measured
2026-08-12: four concurrent `pull_request` matrices, three of the four PRs
`draft=false` behind an interrupted land.

The re-draft is conditional on the head's verdict, which is the design and
not a caveat. The pre-push ready fires only on a head with NO graded run, so
re-drafting a head that already graded green strands it: unmergeable while
draft, and unreadyable because `graded_runs` is no longer zero. Readying it
anyway is the worse repair — `ready_for_review` is a CI trigger, so
recovering a green head would buy a whole matrix where that resume is a free
fast-forward today. So red and no-answer re-draft, green stays ready, and
"could not look" leaves it alone rather than stranding a head on a reading
we failed to take.

`checks-green` is the authority for "is this head green", not `graded_runs`:
the two answer different questions, and the one that counts a failed or
cancelled run as an answer would leave a green head ready only by accident.

A SIGKILLed land runs no trap, so this closes the graceful subset only.

Refs: CLOUD-458
…never fires

`fails rebase` alone is inert: `land` rebases only when the branch is not
already linear, and the stub's default is linear, so four of the new exit-trap
cases sailed past the intended conflict into the fast-forward wait and blocked
on `main-watch`, which never answers by design. A wedged suite emits no `not
ok`, so it read as 463 passing tests for 31 minutes.

The suite's existing conflict case already pairs the two levers; this copies
that pairing rather than inventing a second way to reach the same stop.

Refs: CLOUD-458
@wenzowski
wenzowski marked this pull request as ready for review August 12, 2026 07:08
@wenzowski
wenzowski force-pushed the claude/groom-cloud-420-1bbuf0 branch from 758e9cb to 2414d5c Compare August 12, 2026 07:08
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 2414d5c into main Aug 12, 2026
7 checks passed
@wenzowski
wenzowski deleted the claude/groom-cloud-420-1bbuf0 branch August 12, 2026 07:17
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