Repository navigation
feat(gate): refuse Done while the issue still has a pull request open (CLOUD-468) - #370
Conversation
CLOUD-468 A merged PR is read as its issue's completion, but an issue can carry N PRs — Done is reachable with the work half-landed
Why "PR #366 is merged" reads as "CLOUD-420 is done". It is not. CLOUD-420 carries four PRs — #362, #363, #366, #368 — and #368 is open and draft. It is the issue's own "below it, for free" bullet: the Measured on the board itself, 2026-08-12. CLOUD-420's
A premature Done, reversed 35 minutes later by hand. The reversal is the evidence: the inference was made, acted on, and had to be undone by a human. It was then made independently a second time, in a session reading the same board — two occurrences of one mechanizable error inside six hours. This is the threat model, not a novel class. Batten exists for honest error: the wrong entity, time, or completion signal. A merged PR is a completion signal for a diff; the board treats it as one for an issue. Those coincide only when the issue has exactly one PR, and nothing checks that they do. Non-negotiable 2 — a rule without a runnable gate is half a change — and there is no gate. Why the existing gates do not cover it. CLOUD-192 is about when the automation fires (merge rather than release); this is about whether the transition is licensed at all given the issue's own attachments — an issue with an open PR is not Done on any schedule. CLOUD-431 is an agent certifying a Ready block it wrote itself, a different failure at the other end of the lifecycle. Refinement — Ready
Test obligation
Regression fixture: CLOUD-420's real shape — four attachments, three merged, one draft — so the incident that motivated this cannot recur silently. Mutation-checked per CLOUD-418. Commit / bump (§6): Blockers (§8): none. Acceptance
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-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. It happened again, live, while building the landing lease (CLOUD-393). A concurrency test was written for a real race — 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 Refinement — Ready
Test obligation The mechanism must catch the case that motivated it: the Commit / bump (§6): Blockers (§8): none. Acceptance
CLOUD-192 The board transitions to Done on merge, not on release — In Review is never occupied
Why. The board is the observability surface, and its states are defined against a trunk-based model: In Review = landed on Measured. PR #103 merged at The end state converged here only because release-plz happened to release promptly. The transition is keyed on the wrong event, so the failure is latent rather than absent:
This also supersedes a recorded observation in Acceptance.
Implementation, 2026-08-13. Re-measured before touching anything. The defect is still live and unchanged from the original CLOUD-168 observation: CLOUD-499's state history reads The two halves have different owners, and only one is code.
So the repo ships the half it can hold: the consequence. The board was corrected rather than grandfathered. Piping the full Done closure (159 issues) exited 1 and named five: CLOUD-404, CLOUD-484, CLOUD-491, CLOUD-495, CLOUD-499 — each landed, each in no tag. All five moved to In Review, Fewer than the 50 unreleased commits would suggest, because an issue is judged by its most-released ref: work carrying several PRs, some shipped, passes. That is CLOUD-468's question and this gate deliberately does not answer it. Notes. If the integration cannot key The automation is not simply slow: What that rules in and out:
The untested hypothesis is Linear's closing vs contributing PR distinction, which the settings page names in its own copy. #398's body carries It is not settled by the earlier counter-evidence in the memory (#131 completing on merge with only a RESOLVED: it is the closing keyword. The controlled probe ran on this issue, and the result is unambiguous.
#404 is the confirming instance rather than a repeat: it is the PR that ships One variable. Same repository, same branch name ( That also retires the So the merge-side transition is reachable, and reaching it is a repo-side change: the PR body must name its issue with a closing keyword. That ships with a gate rather than a convention, per non-negotiable 2 — a rule with no runnable mechanism is half a change, and this specific rule is invisible when broken, which is exactly the shape that decays. Superseded next step (kept for the record): If it moves to In Review and a Status against the four acceptance bullets.
All four bullets hold, and the issue is Done on the definition it argued for — released, not merely merged. It reached each column by the mechanism that column is supposed to have: |
…ing it Measured twice on 2026-08-12, on two branches that touch no part of the landing loop: PR #354 (a fuzz crate) and PR #370 (a board gate) each lost a full verify and a lap to `not ok THE DEFECT: a lease sighted before it expired is taken on the first check after`. Re-run alone, the case passes. It was never the branch and never the code under test — it was the test's own clock. The race is in the SETUP, not the measurement, which is what the original comment missed while correctly calling the row above it "the flake-proof half". The case pins a 4s TTL and then depends on a wall-clock ordering across separate process launches. `test:bats` runs under `rush --jobs` (CLOUD-386), and the process can be descheduled for longer than the whole TTL — at which point the "sight the live lease" acquire is RIGHT to succeed, and it steals instead of sighting. The output said so plainly: `took the lease 0s after ... stopped holding it`. The assertion graded the runner's scheduler. That is worse than an ordinary flake because it sits in `verify`, on the landing path, in the suite that gates every branch in the fleet. `land` says "reproduce and fix locally", nothing reproduces, and the reliable way through is to run `land` again — the reflexive drive-to-green AGENTS.md forbids, arrived at honestly. So the precondition is established rather than assumed, and never asserted through (CLOUD-249): a sighting acquire that succeeds means the lease had already expired, so no sighting happened and there is nothing to measure. SETUP is retried — never the measurement, which would be drive-to-green in the test — because a plain skip would fire often enough to erase the coverage, this having raced twice in one day. Three attempts, then a skip naming the reason. The TTL stays short deliberately. Raising it widens the window without removing the race, and every second added is paid on every run of the suite. Both halves of the obligation are checked, since either alone would let the repair convert a flaky check into one that cannot fail: A. Against the pre-fix land-lock (sighting recorded only once expired, steal at ~9-12s) the repaired case still goes RED. B. With a deschedule past the whole TTL injected before the sighting, it SKIPS naming the reason — never fails, never silently passes. `LAND_LOCK_UNDER_TEST` is added so that harness mutates a COPY: an in-place mutation makes a corrupted commit reachable from any concurrent `git add -A`, which staged a mutant into a pushed commit earlier the same day (CLOUD-418). The harness hashes both tracked files before and after and requires them byte-identical. Refs: CLOUD-448
31626ca to
931abc8
Compare
The table tracks adopted or vendored tools. Three rows survived tools whose adopt verdicts closed negative: the file-shape linter, the red-green-refactor/judge prior art, and the hook-file mapping generator, whose wiring derives from the Harness enum instead. A mined design creates no dependency and no license obligation. cargo-deny and ripsecrets stay: one gates today, the other is a pinned dependency of the secrets rule kind. Refs: CLOUD-530 Claude-Session: https://claude.ai/code/session_01PKrKv9gwfiB7MKbRzZGZSV
931abc8 to
6d6cd5a
Compare
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
6d6cd5a to
346783a
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 — #362, #363, #366, #368 — and #368 was open and draft:
A premature Done, reversed by hand 35 minutes later. The reversal is the evidence: the inference was made, acted on, and undone by a human. It was then made independently a second time, in 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.
It has since happened a third time, after this issue was filed: CLOUD-110 was set Done at 17:40 on 2026-08-13 and reversed to In Progress at 17:44, with PR #341 open and draft.
This is the threat model rather than a novel class: honest error about a completion signal. Non-negotiable 2 — a rule without a runnable gate is half a change.
Named
done-pr-check, notdone-checkCLOUD-468 §3 names
mise-tasks/done-check. That name was taken while this PR sat unlanded: CLOUD-192's release-containment gate — no release tag contains a Done issue's commits — landed there inf5dd5eaand is already wired intorelease-plz.yml, AGENTS.md andmem:workflow/board-states. The rebase surfaced it as an add/add conflict on both the task and its suite.Merging the two was considered and refused on evidence. Their stdin contracts are incompatible in the direction that matters:
release-plz.ymlpipes Done rows carrying no.pulls, which this gate answers 2 for by design — the clause that stops an unread PR being the cheapest route to Done. Folding them together would turn a green release path red for a reason unrelated to releases, and scoping the rule to "judge only when.pullsis present" would delete the acceptance clause instead. Two questions about one transition, two gates; a caller may run both. The landed-and-wired name keeps it. Recorded on CLOUD-468.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 — an issue with an open PR is not Done on any schedule.
graph-checkowns the ready frontier and theIn Review ⇒ linked PRrule, which is the entry to review rather than the exit from it.landed-checkanswers "this ref is onmain". None of them can see a second PR still in flight.What it decides, and what it refuses to guess
Arithmetic only: N attached PRs, k open ⇒ not Done. It never judges whether the merged ones did the work — not computable, and would make this a judge rather than a gate (CLOUD-93, non-negotiable 3). The PR filter is
claim-check's character for character, so the two agree by construction on what counts as a pull request rather than by two authors agreeing today.Two decisions worth stating:
Verification
19 rows in
tests/done-pr-check.bats, pure function of stdin — no network, no credential — so they run unconditionally in the gate rather than needing live board data.Registered in
MUTANT_GATES, which the first draft omitted. The five mutations were run by hand and reported 5/5; nothing declared them, somise run mutantwould never have re-run them and the proof would have decayed into a claim. They are#MUTANTrows now —draft-not-open,open-state-ignored,no-pr-licensed,absent-state-licensed,filter-host-unanchored— and the repo-wide run is 25/25 caught.The last of those is the genuine survivor the first pass found: widening the PR filter to
pull/[0-9]+passed every existing row, because the number capture already requires a leading slash and so rejectshow-to-pull/123on its own. The host restriction was the half nothing exercised — without it a forge link on another host would read as a blocker. That row now exists, and the mutant is caught.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 (recorded on CLOUD-418).Driven against the live payload, which is also a committed regression row:
That is the case a human had to catch by hand, now decided by an exit code.
Not wired to a hook yet
This ships as a runnable gate the caller invokes, matching
claim-checkandgraph-check. Wiring it into the board automation is a separate question — the automation is what fires the Done transition, and it is not something this repository controls.Closes CLOUD-468
Refs: CLOUD-420, CLOUD-418, CLOUD-192