Skip to content

fix(land): cut the cost of a lap without weakening a verdict (CLOUD-399, 345, 376, 404, 413, 418, 420, 470, 475, 476) - #375

Merged
wenzowski merged 12 commits into
mainfrom
claude/landing-loop-followup-zph9ay
Aug 12, 2026
Merged

fix(land): cut the cost of a lap without weakening a verdict (CLOUD-399, 345, 376, 404, 413, 418, 420, 470, 475, 476)#375
wenzowski merged 12 commits into
mainfrom
claude/landing-loop-followup-zph9ay

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Follow-up bundle on #372, landing as one cycle. Every saving comes from spending
fewer runs to get the same independent verdict on the exact SHA that lands — no
guard is weakened, and no unknown is converted into an answer.

What is here so far

Issue Change
CLOUD-399 Laps (metered) priced at 2, lease waits (free) at 64. The two exhaustions carry distinct exit codes, so a caller tells a saturated fleet from a runaway branch by status rather than by prose.
CLOUD-345 Every fetch prunes; the push rejection splits by cause, so a branch our own merge deleted is told to prune rather than to force.
CLOUD-420 The lease precondition's workspace setup is guarded explicitly instead of reaching fail-open by accident, and a broken workspace no longer reports as land-lock's answer.
CLOUD-418 mise run mutant: a declared one-line corruption applied to a throwaway copy must turn a named case red. The enforced set is data; gaps are filed (CLOUD-480), not silent.
CLOUD-376 CI_ANSWERED_CONCLUSIONS declared once and read by both readers. The catch-all is gone, so an unknown conclusion holds the poll open instead of reading red.
CLOUD-470 The declination is asked of land-lock authorises — the verb the runner consults — instead of a second predicate over the run list.
CLOUD-475 finding-sink-check distinguishes opening a row from annotating one, so a finding commented onto a Done issue no longer scores durable.

Still in flight on this branch: CLOUD-413 (honour the rate-limit headers rather
than a guessed margin), CLOUD-404 (a provisioning flake is "could not look", and
the failed jobs are re-run rather than the lap re-bought), CLOUD-383 (the FIFO
rendezvous that removes the last bash-4 construct), CLOUD-476 (hk install
performed and asserted), and the CLOUD-448/450 residual.

Verification

Suites green locally at each step: tests/land.bats (89), tests/checks-green.bats,
tests/ci-wait.bats, tests/ci-lease-precondition.bats (21),
tests/finding-sink-check.bats (21), tests/mutant.bats (10).
mise run mutant reports six declared mutations across five gates, every one caught.

Draft, so CI does not run yet.

@linear-code

linear-code Bot commented Aug 12, 2026

Copy link
Copy Markdown
CLOUD-399 `land` cannot converge at current fleet cadence: 16 laps, 5 `/fast-forward` comments, every one lost the race to a moving `main`

Why

Measured 2026-08-11 landing #306 (a one-file docs change with no conflict surface): four consecutive land invocations, ~28 laps, ~28 CI runs, zero merges. verify was green on every lap and CI graded green on every lap that survived to be graded. The branch never failed a gate — it simply never stopped being BEHIND. Eight of eight /fast-forward comments lost to a main that moved while the bot was silent.

The cause was measured, and it is not latency. Over every fast-forward.yml run available, 2026-08-11 21:31→22:01Z — 400 runs, 248 executed, 152 skipped by the author_association gate:

quantity value
dispatch lag (created → started) median 0s, max 0s
bot answer time (started → conclusion) median 12s, p99 23s, max 23s
conclusions 243 failure, 5 success, 152 skipped

The bot answered all 248 attempts within 23 seconds. 248 attempts in 30 minutes producing 5 merges is a thundering herd, ~2% success per attempt — not a slow bot. That retires candidate #2 (instrument bot latency and wait on the merge): the quantity it proposed to measure is 12s.

Candidate #1 shipped. The landing queue is CLOUD-393's rolling lease, landed in #340. Re-sampled 2026-08-12: the baseline holds (245 failure : 6 success over the same window), and lease turns are honoured by every session that takes one.

What remains is candidate #3, plus a cost inversion the lease exposed.

  1. The two backstops are tuned backwards. LAND_MAX_LAPS bounds CI matrices (~10–20 runner-minutes each); LAND_LOCK_MAX_WAITS bounds lease waits (a poll, no runner). Both default to 8 — so ~2 runner-hours of metered spend is authorised against 16 minutes of free waiting. Measured on fix(tasks): read a cancelled required check as no verdict, not as red #302, 2026-08-12, four active sessions: 5 waits lost and 3 laps entered in 22 minutes, i.e. the expensive budget drains first. The trade the defaults should express is many free waits, few paid laps.
  2. A saturated fleet and a runaway branch exit the same way. Both end in die, and a backstop that fires on the ordinary busy-fleet path is a broken signal: an agent reading it cannot tell "you have a real problem" from "wait and try later". land-lock-check already separates a saturated fleet from a wedged lease; land's own exit does not carry that distinction.

Refinement — Ready

  • Source of truth (§1). land's own counters — laps entered and consecutive whole lease waits lost — each already tracked in the loop. The cost side is arithmetic over the CI matrix (6 jobs) versus a poll (0 jobs); no number is typed by hand beyond the measured lease turn.
  • Mechanism (§3). Re-tune the defaults so each backstop bounds its own resource honestly: LAND_MAX_LAPS down to the number of CI matrices a landing may buy (2), LAND_LOCK_MAX_WAITS up to the queue depth a contended fleet reaches (~64, i.e. ~2h at the observed 2–5 minute turn). Both stay env-overridable, so a caller can spend differently without editing the task.
  • The two exhaustions become distinguishable (§3). Exhausting waits is "this branch never got a turn — the fleet is saturated"; exhausting laps is "main moves faster than a lap takes". They get distinct exit codes rather than distinct prose, so a caller decides on a status instead of parsing a message, per the one output contract.
  • Deliberately not in scope (§2). The lease itself, its TTL, its beat, and every steal precondition. Bespoke retry logic around land, which AGENTS.md forbids and which would not help — the loss happens after the comment, not before it. Raising limits without re-pricing them: more laps at ~2% per-lap success buys CI minutes, not merges.
  • Why this is not merely configuration (§3). A default is what every unattended session actually runs, and this task is run unattended by design. The inversion above is not a preference — it authorises the wrong resource by an order of magnitude, and no gate today notices.

Test obligation

tests/land.bats: exhausting the wait budget reports saturation and spends zero CI matrices; exhausting the lap budget reports the runaway condition; the two exits differ by code, not only by message. Mutation-checked per CLOUD-418 — swapping the two verdicts must turn the cases red — and the existing lap-count and stop-count assertions must still hold.

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

Blockers (§8): none. CLOUD-393 landed in #340 and supplied the queue this issue's diagnosis called for.

Acceptance

  • A branch that never wins the lease ends by reporting saturation, having bought no CI matrix it did not attempt.
  • The two exhaustion states are told apart by exit code.
  • Measured: CI matrices spent per landed PR, against CLOUD-393's ~2.9 baseline.

Review in Linear

claude added 12 commits August 12, 2026 21:38
A survey that classifies a corpus needs cases whose answer is known because
getting them wrong is a mistake already made, and the run must fail when one
misclassifies. Round one of this survey matched policy files by filename and was
wrong in both directions; round two was built to make that unrepresentable and
reproduced the same blind spot in a new costume, fetching only paths guessed in
advance. The canaries are what caught it.

The transferable half is the sentence about instruments: absence of evidence is a
claim about the instrument before it is a claim about the world. The corollary for
any classifier is that a name may decide what gets read, and only the text decides
what a thing is.

Refs: CLOUD-315
The command shipped with the framing the memory beside it had already
dropped: "the honest signal that you are past it is a rising re-verify rate",
plus a pointer to `mem:workflow/agent-fanout` for "the current cap", which
that memory no longer states in those terms.

Two authorities disagreeing about the same fact, in the file whose §1 was
"one authority each, no duplication" — and the command is the reachable one,
so a dispatcher invoking `/plan-fleet` got the superseded model while the
memory it points at carried the current one.

Replaced with the cost table the memory already holds: re-verifying is free
because `land` laps without a model turn, and what actually costs is a rebase
conflict, CI minutes on a voided run, and tokens. Objective restated as pace
of landed work per token, with the saturated-queue target and the CSMA/CD
framing, and the CI-minutes row now names CLOUD-369 as unbuilt rather than
implying a control exists.

Refs: CLOUD-367
…nheriting a spent window

Two defects in `plan-hold`, both measured 2026-08-12 in one session.

THE ANSWER WAS INVISIBLE. `plan-hold-guard` gates `PreToolUse` on
`ExitPlanMode|AskUserQuestion`; `plan-hold-release` listens on
`UserPromptSubmit`. Those are different event classes, and a human answering
either of those two tools produces a TOOL RESULT, not a prompt. So for the exact
case the mechanism exists to serve, the release could never fire: a hold armed,
an `AskUserQuestion` answered, the hold still live afterwards, ended by removing
its sentinel by hand. CLOUD-451's third acceptance bullet, unmet since it landed.

`plan-hold-release-tool` is the missing half, on `PostToolUse` over the same two
tools the guard already names. No classifier: `plan-hold-release` must decide
whether a prompt came from a person because `UserPromptSubmit` carries machine
turns too, while here the provenance is structural — the event fires only after a
tool whose whole purpose is to ask a human. The `tool_name` check is belt to the
matcher's braces, because a matcher widened later would otherwise turn every tool
call into a release. CLOUD-435's cost argument does not reach it for the reason
it does not reach `plan-hold-guard`: these two tools fire at most once per turn,
and it is invoked by path, so no task-runner startup is paid.

THE WINDOW ERODED SILENTLY. A second launch answered "already held" and exited,
which keeps one sleeper and hands the new handoff whatever is left of the old
one's cap. Some answers reach neither release path — a reply typed mid-turn
arrives `<`-wrapped and reads as a machine turn — so that handoff's hold never
releases, the next inherits a partly spent window, and eventually one is guarded
by a hold about to cap and the container is reclaimed while somebody is reading.
Nothing blocks, so nothing announces it. A second launch now releases the
incumbent and arms fresh: at most one sleeper still, and every handoff gets the
whole cap. Released, never signalled — a killed hold wakes nothing.

Deliberately NOT touching `plan-hold-release-check`'s `<`-prefix classifier. The
mid-turn false negative is real, but widening a literal without measuring it over
real transcripts is the mistake CLOUD-252 and CLOUD-323 exist to prevent. The
erosion fix removes its compounding cost; the false negative itself stays
recorded on CLOUD-485.

Refs: CLOUD-485
Every bound in this memory was a bound on landing, and its cap of 2 is
derived from land contention. An agent that researches, reviews, or drafts
never lands, so that derivation says nothing about it — and the silence
read as absence of a constraint rather than absence of a derivation. Eight
drafting agents were launched into it.

The binding constraint for that shape is per-agent fixed cost, which is
paid once per agent and multiplies with N. The measured floor now has a
home: 63,848 tokens for one agent to fetch a single issue and run one
lint, for near-zero work. Each prompt named eight artifacts as required
reading, so that floor was paid eight times over before anything happened.

Three rules follow — digest rather than cite, checkpoint at each unit's own
gate because scratch dies with the container, and pilot one unit through
its durable write before widening — and the section says outright that
none of them is gated, because the predicate they need measures a session's
real spend and that is out of tree (CLOUD-95). Assumed enforcement is
worse than stated absence.

Also corrects the implementer contract in the same file: it still listed a
hand `gh pr ready` before `land`, which is the defect CLOUD-247 named, and
`land` has been the only readier since.

Refs: CLOUD-289
…wo exhaustions (CLOUD-399)

`LAND_MAX_LAPS` bounds CI matrices (~17 job-minutes each); `LAND_LOCK_MAX_WAITS`
bounds lease waits (a conditional poll, no runner). Both defaulted to 8, which
authorised ~2 runner-hours of metered spend against ~16 minutes of free waiting
— and measured on #302, the expensive budget drained first. Laps to 2, waits to
64, so the defaults express many free waits and few paid laps. Neither is a
clock; both stay env-overridable.

Both exhaustions also ended in `exit 1`, so a saturated fleet ("wait, and land
later") and a runaway branch ("main moves faster than a lap takes") were
indistinguishable to anything but a human reading stderr. They now carry their
own codes through `die_with`, which `die` delegates to so the other eighteen
call sites are unchanged.

The stop-count sensor counts both spellings. Counting only `die "` would have
read the split as two stops removed, and would have let a future `die_with` stop
be added completely uncounted — the exact blindness that assertion exists to
prevent, reintroduced by the change that split the helper.

Refs: CLOUD-399
…one (CLOUD-475)

One flat durable class counted `save_comment` as a home with no term for
the target's state, so a turn that finds a defect in landed code, comments
it onto the Done issue that shipped it, and files nothing scored durable
and passed. The board then has no open row, no sweep visits it, and no
gate notices: the finding is durably RECORDED and permanently UNSCHEDULED.
The gate written to catch a stranded finding was cleared by the exact act
that strands it.

"Refuse a comment on a Done issue" is uncomputable here — no tracker
credential exists in a hook, as for `claim-check` — so the predicate keys
on the call shape the transcript already carries: `save_issue` with no
`id` OPENS a row and is open by construction; `save_comment` and
`save_issue` with an `id` ANNOTATE one that may be terminal. A home needs
at least one opening. Memory and document writes keep their standing.

Written once before against the single-pass scan and stranded there; this
is the same rule re-anchored onto the two-pass structure CLOUD-479 landed.
The `#id`/`#noid` stamp rides pass 2's existing jq, so nothing slurps the
transcript and no turn is built whose verdict is discarded — the cost
property that rewrite bought is untouched. The two classes are matched in
one alternation, never two greps joined by `||`, which `pipefail-grep-check`
refuses and is right to.

The refusal names the practice, not the rule: an author who commented the
finding correctly believes they wrote it down, so "not durable" reproduces
the confusion. What they are missing is an open row.

Refs: CLOUD-475
…use (CLOUD-345)

When a PR merges GitHub deletes the head branch, but a plain fetch never removes
`refs/remotes/origin/<branch>` — so the tracking ref survives, still naming the
SHA that landed. Reusing that branch name then hits a `--force-with-lease` whose
expectation names a ref the remote does not have, and the push is rejected as
`stale info` forever: no number of laps clears it, because every lap re-fetched
without pruning. Both of `land`'s fetches prune now, including the merged path —
which is the instant the phantom is minted.

The rejection named the one cause that was usually not true, and named it toward
the dangerous action. "Someone else moved the branch" describes a concurrent
writer, whose correct response is caution; the common case is our own merge
having deleted the branch, where `git log HEAD..origin/<branch>` is empty and
every check an operator would run says forcing looks safe, for the wrong reason.
The split is mechanical — absent from the remote is a different state from
present at an unexpected SHA — and `--force-with-lease` is still what pushes in
both, so the lease is not traded for a worse bug.

The suite keeps the concurrent-writer row as the anti-vacuity half: a genuine
rival must still get today's caution and must never be told to prune.

Refs: CLOUD-345
…D-420)

`set -euo pipefail` became `set -uo pipefail` to keep the header's promise —
"AND IT NEVER EXITS NON-ZERO" — and nothing exercised the case that promise
exists for. Eighteen cases covered `gh` and fetch failures; every one handed the
script a working `RUNNER_TEMP`.

The whole setup chain was unguarded: the `RUNNER_TEMP` fallback, `mkdir -p`, the
redirect, `chmod +x`, `git init`, `remote add` and `config --local`. It reached
fail-open by accident of the `*)` arm at the bottom, which then reported
`land-lock answered <rc>` — attributing a workspace failure to a predicate that
was never consulted, on the line a human reads when the fleet misbehaves.

Guarded explicitly now, with the same verdict (run) and the cause named honestly.
Two rows cover it: a `RUNNER_TEMP` that cannot hold a directory still exits 0,
and the message no longer blames land-lock. Both go red with `-e` restored, which
is what makes them a test of the promise rather than of the happy path.

Refs: CLOUD-420
…t once (CLOUD-418, CLOUD-376, CLOUD-470)

These three land together because they cannot be split: the mutation rows added
here name the very cases the other two changes add, and a `#MUTANT` row naming a
case that does not exist yet fails the sweep by design.

CLOUD-418 — `mise run mutant`. The obligation was "a rule ships with a runnable
gate" — a gate that EXISTS. Nothing required evidence it DISCRIMINATES, and a
test passing on both the fixed and the broken code satisfied every other rule
here. That failed four times (CLOUD-235, CLOUD-352, CLOUD-401), the last one
live: a concurrency test written for a real race was green, and stayed green when
the bug was restored. Now a declared one-line corruption is applied to a
throwaway copy and the named case must go red.

The enforced set is data: `$MUTANT_GATES`. A gate IN it with no declaration fails
— that is the anti-vacuity term, without which the task reports success over a
set it never touched. A gate not in it is a filed row (CLOUD-480), never a silent
exemption. Seeded with the five gates this bundle touches, so its
"mutation-checked per CLOUD-418" obligations are cashed rather than cited. It
mutates a copy of the WORKING tree, never the tracked file — mutating in place
staged a mutant into a pushed commit on 2026-08-12 — and refuses two evasions an
inert or misnamed row would otherwise buy. Both were hit on its first run, and
one exposed a test written minutes earlier that did not discriminate.

CLOUD-376 — `CI_ANSWERED_CONCLUSIONS` in mise.toml [env]. `checks-green` and
`land`'s `graded_runs` each hand-maintained a conclusion list, in agreement only
by a paragraph of comment in each file — precisely the guarantee that had already
failed, since `neutral` was missing from one until #302 added it and nothing
detected the gap. The catch-all is gone with it, so an unknown conclusion holds
the poll open instead of being reported red against a head nothing judged.

CLOUD-470 — the declination is asked of `land-lock authorises`, the same verb the
runner's own precondition consults, rather than re-derived from a raw
`conclusion == "cancelled"` read. Two authorities for one fact is the CLOUD-351
shape. What that verb cannot answer is recorded in the code rather than papered
over: it judges the lease now, so a `cancel-in-progress` cancellation is no
longer caught — that is a superseded run, not a declined one.

Refs: CLOUD-418, CLOUD-376, CLOUD-470, CLOUD-480
…e (CLOUD-413)

Measured on PR #323: 24 laps across three invocations, never merging, and not one
lap failed for any of the three reasons `land` is documented to stop on. Several
refusals were a 403 rate limit, which `land` could not tell from "main moved" —
so its response to being rate-limited was to generate more of exactly the request
that was rate-limited, each retry costing a `verify`, a CI run and another
comment.

The comment POST now asks with `-i` and reads the response's own headers, which
is where the delay is stated: `Retry-After` if present, else `X-RateLimit-Reset`
when remaining is 0, else a floor. Reading a second endpoint for that number
would be one more request against the limit that just refused this one. The body
split is `main-watch`'s idiom, since `gh` cannot filter a response it is also
printing headers for.

Nothing here gains a clock. The bound stays `LAND_ANSWER_MAX_UNKNOWNS`, a count —
the one place `mem:github-rest-etiquette` says a retry cap belongs — and the pause
is a delay before re-asking on a path that had already decided to lap. Lapping
with no delay was never backoff; it was the same request again.

The exhaustion message also stops telling the reader to run `gh api rate_limit`
for a number the code was already handed, and states the reset time instead. Over
those 24 laps its old diagnosis was wrong twice over: 7 of 8 laps in one
invocation reached green CI, and several refusals were the limit rather than
`main` at all.

Refs: CLOUD-413
…UD-448)

`tests/land-lock.bats` carried an acknowledged residual: the steal case graded
seconds between the previous holder's expiry and the steal, and both ends of that
delta are instants on one clock, so a deschedule landing between them inflated
it. Under the parallel runner that fired on roughly 2 of every 4 `verify` runs,
and it blocked CLOUD-274's landing directly — `land` refused to push on a
`verify` whose only failure was this case, behaving exactly as designed over a
signal that was wrong. A flaky gate is a bypassed gate.

The case's own header named the fix and deferred it: report the number of PROBES
`acquire` spent, which is a count on no clock at all. `acquire` now does. The
promise was never really about seconds — "a dead lease costs one extra beat" IS
"the steal lands on the first post-expiry probe" — so the assertion states that
directly and no amount of load can move it. The seconds stay in the sentence
because they are what a human reads; nothing grades them.

Not retried, not serialised, not widened: all three were ruled out on the issue,
and each would have encoded the flake as expected behaviour.

Refs: CLOUD-450, CLOUD-448
…8, CLOUD-376)

Four defects that cancelled out, so none of them was visible.

`tests/checks-green.bats`'s CLOUD-376 row asserted the output does not
contain "red" — against a message that reads "requi-red check(s) with no
verdict". It could never pass, and it did not: it was red from the commit
that added it.

The mutation aimed at that case targeted `!($2 in isanswer) ? 3` in the
RANKING expression, which only decides an unorderable pair. The case's
fixture is orderable, so the mutant could not change its outcome. The row
never discriminated; it read as caught only because the case was
permanently red.

`mutant` itself never checked the other half. It refuses an unappliable
mutation, an inert one and a filter naming no case — but not a case that
was already failing, which makes every mutation aimed at it read as
caught. That is the vacuous pass this task exists to refuse, one level up
from where it was refusing it.

And `tests/mutant.bats` writes bats fixtures containing `@test` lines at
column 0, which the `test:bats` count gate reads as cases of its own:
1333 of 1337 reported, failing a gate whose whole job is to notice a suite
that ran fewer tests than it declares. Tab-indented heredocs now, so a
file this suite WRITES is not counted as a case this suite HAS.

So: the assertion names the verdict (`is not green`, and the exact
no-answer line) rather than a substring of it; the row targets the verdict
site, where flipping it really does turn the case red; the harness runs
the named case unmutated first and reports `case-already-red` rather than
counting it; and the count gate sees eleven cases, which is how many there
are. Costs one filtered bats run per row, which is what an anti-vacuity
term is worth.

Refs: CLOUD-418, CLOUD-376
@wenzowski
wenzowski force-pushed the claude/landing-loop-followup-zph9ay branch from 12b8b46 to 6261a5b Compare August 12, 2026 22:17
@sonarqubecloud

Copy link
Copy Markdown

@wenzowski
wenzowski marked this pull request as ready for review August 12, 2026 22:38
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 6261a5b into main Aug 12, 2026
15 checks passed
@wenzowski
wenzowski deleted the claude/landing-loop-followup-zph9ay branch August 12, 2026 22:47
wenzowski pushed a commit that referenced this pull request Aug 13, 2026
… the board

Two shapes that reached something expensive with nothing checking them. They
land together because both declare mutants and both widen $MUTANT_GATES.

CLOUD-488 — run-shape-guard refuses a `git commit` that cannot obtain a
message. `githooks(5)` runs pre-commit BEFORE git asks for the message, so a
commit that can never get one still spends the entire gate and only then exits
with "Aborting commit due to empty commit message". Measured landing #375:

  git add -A && git commit -F - >log 2>&1 && mise run land >l2 2>&1 <<'EOF'

The heredoc binds to the LAST element of the `&&` list, so `land` got the
message and `git commit -F -` got the harness's /dev/null. ~4 minutes of gate on
a doomed commit, and killing it took `kill -9` on the process group because hk's
children carry its command line.

Judged per element, which is what makes it decidable: the opener is in the
command string and absent from the element that needed it. A heredoc that
genuinely binds here is allowed, as is `< file` and `<<< "$msg"` — any redirect
into that stage is a message source, so one test covers all three. The second
shape is a `git commit` naming no message flag at all, which opens $EDITOR in a
non-interactive call and blocks until the harness kills it. `git -C <path>
commit` is deliberately NOT caught: CLOUD-199 measured that a guard with false
positives gets bypassed, and a false negative is the direction to be wrong in.

CLOUD-512 — nothing gated a move to In Review. graph-check already decides it
correctly and CLOUD-309 bound it to the release sweep; the transition itself
passed through nothing, so a wrong label survived until somebody thought to run
the gate. Measured 2026-08-13: a sweep after #375 put CLOUD-480 In Review on a
`Refs:` trailer that named it as the still-open gap. It sat wrong for 4.5 hours,
and graph-check refused it in one invocation once asked. The gate was never
wrong; it was never consulted.

This adds a TRIGGER, never a second predicate — a copy of `in-review-no-pr` here
would be the CLOUD-351 shape. A hook has no tracker credential, so the guard
cannot look an issue up and does not try: graph-check records which ids it
judged, on the success path only, and the guard demands the id be among them.
Keyed to the SET rather than the turn, because a bare "graph-check ran" receipt
is satisfied by judging one clean issue and then sweeping fifteen — which is the
sweep this exists to stop. Bounded in age for the same reason issue-read-guard's
read is, and the two tests are one grep over whole LINES: a stale line naming
this issue and a fresh line naming others must not combine.

graph-check's tests now run in a throwaway repo. Run from this checkout they
minted adjudications into the real .git/batten-receipts/ — receipts that would
have authorised a live session's moves over fixture ids. Found on the first
green run of the new rows.

Not in this change, and each for a stated reason: CLOUD-454's
`mention-without-relation` is parked, because the measurement its own §2
demanded falsified it — the rule fires on 0 of 50 in-set mentions, since Linear
DERIVES relatedTo from a mention rather than recording it separately (measured
on CLOUD-480, where four relations appeared from a comment's prose alone), so it
would ship as a rule that cannot fail. CLOUD-427 is Rust core with a much
heavier test obligation, and CLOUD-480 now spans ~80 files in the directory this
change edits.

Refs: CLOUD-488, CLOUD-512
wenzowski added a commit that referenced this pull request Aug 28, 2026
…locks

Closes CLOUD-613
Closes CLOUD-723
Closes CLOUD-1112

CLOUD-613 asked one question and reserved it for its owner: is heredoc binding
worth a permanent parser surface, or does the predicate stay in bash forever?
Answered on 2026-08-28 — model it. This is that.

THE PREDICATE IS NARROWER THAN "MODEL HEREDOCS", which is what makes the surface
affordable. `run-shape-guard.sh:372-440` decides per element: `git commit` with
`-F -` and no redirect in that same element. So the parser owes two things and
not a shell — drop heredoc bodies before tokenizing, and record per-segment
redirect binding — and both fall out of the character walk `hook::segments`
already makes.

Doing it INSIDE that walk rather than as a pre-scrub is the load-bearing choice.
A pre-pass has no quote state to consult, so `echo "<<EOF"` reads as an opener
and starts a skip to a delimiter that never comes: the rest of the command
vanishes, the gate stops looking, and the suite stays green over it. The same
position decides quoting, redirection and openers, once.

BOTH DIRECTIONS, AND THE SECOND IS A LIVE DEFECT. CLOUD-723 is this parser
getting heredocs wrong the other way: every `pipeline` row decides over these
segments, so a `;` inside a heredoc BODY split the list and `verdict-not-
discarded` refused a correct command. It fired twice in one session, both times
on the command that was writing the rule down. One parser, one fix, and the
maintenance surface the reserved question worried about is bounded by having the
two directions asserted against each other.

THREE PREDICATES, over `input.call.segments` rather than a hand-rolled scrub:

  unsatisfiable-commit  `git commit -F -` with nothing bound to ITS OWN element
                        reads /dev/null — and `githooks(5)` runs `pre-commit`
                        before git asks for the message, so the whole gate is
                        spent first (~4 minutes measured, CLOUD-488, PR #375).
  foreground-sleep      the harness kills a foreground call at ~2 minutes, so a
                        patient poll FAILS (CLOUD-482, exit 143 and 144).
  background-timer      a backgrounded `sleep N; tail log` exits on the clock,
                        never on the event — 490 in one session against 523 of
                        524 tasks that notified on exit; 2 changed a decision
                        (CLOUD-821).

The last two are CLOUD-1094's `run-in-background` finding its first consumer.

THE ALLOW IS THE LOAD-BEARING HALF of all three, and each has its own
discriminating negative: `git commit -F - <<'EOF'` in one element, and — for the
wait rules — the same loop twice, differing only in posture.

WHICH IS WHY CLOUD-1112 IS FIXED HERE RATHER THAN CARRIED. That posture pair did
not work: `do sleep 1` resolves to the program `do`, because the look-through
table covers `env`/`timeout`/`sudo`/… and no shell KEYWORD, so the sleep in a
loop body is never reached. `run-shape-guard.sh`'s `resolve()` answers the same
way, and its comment that an element-scoped loop test "would deny every correct
wait" presumes an element it never reaches.

The instinct was to port the gap, on the rule that a migration moves a predicate
rather than changing it — and `filed-over-own-diff` refused the row filed about
the file this branch edits, which is what forced the premise to be checked. It
does not hold: CLOUD-613's acceptance turns on the backgrounded allow being LOAD-
BEARING, and with the gap ported that clause passes VACUOUSLY, the loop being
allowed because nothing resolves the sleep rather than because the exemption
works. Shipping a vacuous acceptance is CLOUD-418's defect, so `keywords` — `do`,
`then`, `else`, `elif`, `time` — is looked through exactly as the wrappers are.
`until`/`while`/`if`/`for` stay out: they introduce a condition list, and
`waits_on_condition` reads them as words.

Two verdicts move, both stricter: a FOREGROUND conditional wait now refuses, and
a backgrounded `for i in $(seq 60); do sleep 10; done` refuses as the timer it
is. This is the one place the two authorities deliberately disagree while both
are live, and it is in the denying direction, so no call gets a weaker answer
from the pair than it had from the guard alone. The bash cannot be repaired —
`shell-retirement` admits only whole-file deletion — and CLOUD-856 retires it.

TWO READINGS THAT LOOK LIKE STYLE AND ARE VERDICTS:

`segment["input-redirect"] == false`, never `not segment["input-redirect"]`.
Rego reads an absent key as undefined and `not undefined` HOLDS, so the negated
spelling denies every commit on an engine that stopped emitting the field. The
comparison allows there — the direction a miss is supposed to fail in.

`input.call["run-in-background"] != true`, never `== false`. `null` is "the host
said nothing", most hosts say nothing, and the shape refused is a WAIT whose
posture being unknown is the case to be strict about. The bash spells it
`[[ "$background" != true ]]` and the two must not diverge while both are live.

A NEWLINE STAYS WHITESPACE, not a separator, and bash disagrees. Promoting it
would change every landed `pipeline` verdict — `mise run verify` on one line and
anything on the next becomes a discarded status — which is a decision about
`verdict-not-discarded`'s reach rather than about heredocs. The cost is stated
rather than absorbed: shell following a heredoc's terminator joins the segment
its opener was written in. It under-denies, never the reverse.

THE BASH IS UNTOUCHED, and that is the ratchet rather than an oversight.
`shell-retirement` admits DELETING a governed file and refuses SHRINKING one, and
`run-shape-guard.sh` keeps a fourth family whose blocker is CLOUD-856. So it
cannot lose these three until it can lose all four, and both authorities decide
them until then. CLOUD-1108 is that gap's row; the predicates here are written
from the bash's own decision table so the two cannot answer differently while
both are live. CLOUD-613's acceptance moved the deletion clause to CLOUD-856
accordingly.

The rule row is renamed `commit-message-obtainable` -> `run-shape`: a row named
for commit messages now also refuses a foreground sleep, and the module is what
it registers.

Refs CLOUD-843, CLOUD-856, CLOUD-1094, CLOUD-1108, CLOUD-199, CLOUD-418
wenzowski added a commit that referenced this pull request Aug 28, 2026
…locks

Closes CLOUD-613
Closes CLOUD-723
Closes CLOUD-1112

CLOUD-613 asked one question and reserved it for its owner: is heredoc binding
worth a permanent parser surface, or does the predicate stay in bash forever?
Answered on 2026-08-28 — model it. This is that.

THE PREDICATE IS NARROWER THAN "MODEL HEREDOCS", which is what makes the surface
affordable. `run-shape-guard.sh:372-440` decides per element: `git commit` with
`-F -` and no redirect in that same element. So the parser owes two things and
not a shell — drop heredoc bodies before tokenizing, and record per-segment
redirect binding — and both fall out of the character walk `hook::segments`
already makes.

Doing it INSIDE that walk rather than as a pre-scrub is the load-bearing choice.
A pre-pass has no quote state to consult, so `echo "<<EOF"` reads as an opener
and starts a skip to a delimiter that never comes: the rest of the command
vanishes, the gate stops looking, and the suite stays green over it. The same
position decides quoting, redirection and openers, once.

BOTH DIRECTIONS, AND THE SECOND IS A LIVE DEFECT. CLOUD-723 is this parser
getting heredocs wrong the other way: every `pipeline` row decides over these
segments, so a `;` inside a heredoc BODY split the list and `verdict-not-
discarded` refused a correct command. It fired twice in one session, both times
on the command that was writing the rule down. One parser, one fix, and the
maintenance surface the reserved question worried about is bounded by having the
two directions asserted against each other.

THREE PREDICATES, over `input.call.segments` rather than a hand-rolled scrub:

  unsatisfiable-commit  `git commit -F -` with nothing bound to ITS OWN element
                        reads /dev/null — and `githooks(5)` runs `pre-commit`
                        before git asks for the message, so the whole gate is
                        spent first (~4 minutes measured, CLOUD-488, PR #375).
  foreground-sleep      the harness kills a foreground call at ~2 minutes, so a
                        patient poll FAILS (CLOUD-482, exit 143 and 144).
  background-timer      a backgrounded `sleep N; tail log` exits on the clock,
                        never on the event — 490 in one session against 523 of
                        524 tasks that notified on exit; 2 changed a decision
                        (CLOUD-821).

The last two are CLOUD-1094's `run-in-background` finding its first consumer.

THE ALLOW IS THE LOAD-BEARING HALF of all three, and each has its own
discriminating negative: `git commit -F - <<'EOF'` in one element, and — for the
wait rules — the same loop twice, differing only in posture.

WHICH IS WHY CLOUD-1112 IS FIXED HERE RATHER THAN CARRIED. That posture pair did
not work: `do sleep 1` resolves to the program `do`, because the look-through
table covers `env`/`timeout`/`sudo`/… and no shell KEYWORD, so the sleep in a
loop body is never reached. `run-shape-guard.sh`'s `resolve()` answers the same
way, and its comment that an element-scoped loop test "would deny every correct
wait" presumes an element it never reaches.

The instinct was to port the gap, on the rule that a migration moves a predicate
rather than changing it — and `filed-over-own-diff` refused the row filed about
the file this branch edits, which is what forced the premise to be checked. It
does not hold: CLOUD-613's acceptance turns on the backgrounded allow being LOAD-
BEARING, and with the gap ported that clause passes VACUOUSLY, the loop being
allowed because nothing resolves the sleep rather than because the exemption
works. Shipping a vacuous acceptance is CLOUD-418's defect, so `keywords` — `do`,
`then`, `else`, `elif`, `time` — is looked through exactly as the wrappers are.
`until`/`while`/`if`/`for` stay out: they introduce a condition list, and
`waits_on_condition` reads them as words.

Two verdicts move, both stricter: a FOREGROUND conditional wait now refuses, and
a backgrounded `for i in $(seq 60); do sleep 10; done` refuses as the timer it
is. This is the one place the two authorities deliberately disagree while both
are live, and it is in the denying direction, so no call gets a weaker answer
from the pair than it had from the guard alone. The bash cannot be repaired —
`shell-retirement` admits only whole-file deletion — and CLOUD-856 retires it.

TWO READINGS THAT LOOK LIKE STYLE AND ARE VERDICTS:

`segment["input-redirect"] == false`, never `not segment["input-redirect"]`.
Rego reads an absent key as undefined and `not undefined` HOLDS, so the negated
spelling denies every commit on an engine that stopped emitting the field. The
comparison allows there — the direction a miss is supposed to fail in.

`input.call["run-in-background"] != true`, never `== false`. `null` is "the host
said nothing", most hosts say nothing, and the shape refused is a WAIT whose
posture being unknown is the case to be strict about. The bash spells it
`[[ "$background" != true ]]` and the two must not diverge while both are live.

A NEWLINE STAYS WHITESPACE, not a separator, and bash disagrees. Promoting it
would change every landed `pipeline` verdict — `mise run verify` on one line and
anything on the next becomes a discarded status — which is a decision about
`verdict-not-discarded`'s reach rather than about heredocs. The cost is stated
rather than absorbed: shell following a heredoc's terminator joins the segment
its opener was written in. It under-denies, never the reverse.

THE BASH IS UNTOUCHED, and that is the ratchet rather than an oversight.
`shell-retirement` admits DELETING a governed file and refuses SHRINKING one, and
`run-shape-guard.sh` keeps a fourth family whose blocker is CLOUD-856. So it
cannot lose these three until it can lose all four, and both authorities decide
them until then. CLOUD-1108 is that gap's row; the predicates here are written
from the bash's own decision table so the two cannot answer differently while
both are live. CLOUD-613's acceptance moved the deletion clause to CLOUD-856
accordingly.

THE ROW ID STAYS `commit-message-obtainable`, which now names the first predicate
to arrive rather than the set. Renaming it reads as `rule-removed` to
`config-lint`, whose only route is a `Weakens:` clause groomed into the issue
BEFORE the work starts — and asserting one inside the change that performs it is
exactly what that gate refuses. The row's comment carries the correction; the
module file is the honest label. Tidiness is not worth laundering a weakening.

Refs CLOUD-843, CLOUD-856, CLOUD-1094, CLOUD-1108, CLOUD-199, CLOUD-418
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