fix: the fast-forward verdict reaches land, and filing joins the gated paths - #390
Conversation
CLOUD-406's third instance, and the last half of it not already landed.
Measured on run 31538830648: Sigstore's TUF metadata endpoint was down, so
`mise install` failed, and two cases went red on a tree that touched neither the
hook nor the toolchain. Their properties are ORDER (doctor runs after install,
before the preflight — CLOUD-218) and OUTPUT (the hook emits no `{"async": true}`
— CLOUD-196). Neither asserts anything about the install succeeding. The
`[ "$status" -eq 0 ]` line was the only coupling, and it is load-bearing for
neither: the hook's `step` helper sets `fail=1` and CONTINUES, so every call is
recorded in `$CALLS` whether the install worked or not.
The stub now intercepts bare `install` the way it already intercepts
`run doctor` and `run container-preflight`, and the cases that genuinely need a
real one opt back in.
THE OLD RATIONALE WAS HALF TRUE, AND THE FALSE HALF IS WHAT COST A RED CHECK.
The header claimed the real exec is what keeps "the two lockfile assertions"
non-vacuous. One of them greps the hook's source text for
`MISE_LOCKFILE=false mise install` and touches no toolchain at all. The other —
"running the hook leaves the tracked lockfile untouched" — genuinely does need
one, because a stubbed install writes no lockfile and so could not observe the
CLOUD-223 residue. So this is the issue's option 2 rather than option 1: keep
exactly one case exercising the real install, plus the end-to-end green case, and
stop everything else depending on it.
ESTABLISH THE PRECONDITION, NEVER RETRY THE MEASUREMENT. `real_install_or_skip`
runs `mise install` directly before the hook. A failure THERE is a statement
about the container's egress, so it skips with that reason; past it, the hook's
own install cannot fail for a provisioning reason, so a red from the case that
follows is a real defect. That is the discriminator option 2 asks for, and the
same split `lock-check` took one layer up. Idempotent and warm, so the second
install costs milliseconds.
Shown able to fail, in every direction the acceptance names:
egress denied (`mise install` forced to exit 1)
-> 8 pass, and cases 7 and 9 SKIP with the reason named.
The two cases that went red on run 31538830648 now pass.
hook stops calling `mise install`
-> cases 4 and 8 redden.
doctor moved outside the synchronous window
-> case 4 reddens.
So the property CLOUD-218 bought survives, which is the half the issue was
explicit about: deleting the assertion is not the fix.
The recovery half of CLOUD-406 needs no commit — CLOUD-404 landed it as
`nonverdict-scan` plus `absorbed_transient`/`charge_transient` on `land`'s red
arm, in a stronger form than §2 specified, and §2's premise that the
discriminator "must not be built" is now false. Recorded on the issue.
Refs: CLOUD-406
CLOUD-491 A live `plan-hold` did not stop a container restart, and nothing records that it was live — the hold ships with no sensor
Why CLOUD-451 landed Measured 2026-08-12, ~22:12 UTC. A hold was armed ( So the container went down with four live tracked tasks, one of them a full Reproduced 2026-08-12, ~23:45 UTC, in the session grooming this issue: a hold was armed, and the container was restarted roughly a minute later, killing it. Two independent instances now, and the second one left artifacts the first did not — see the measurements below. The finding is not "the hold is wrong". It is that the hold cannot be graded. A mechanism that occupies the container leaves no record of having done so, so from inside there is no way to distinguish the hold having been live and reclaimed anyway from the hold having already exited from a platform event no occupancy could defer. All of them produce the same observation — a fresh container — and the issue's acceptance is written in terms nobody can check after the fact. That is the sensor-without-a-gate shape inverted: a mechanism with no sensor. It is why an instance can be reported but not diagnosed, and why the honest first move is evidence rather than a bigger hammer. Measured 2026-08-12 ~23:30–23:46, and it changes the mechanism The first draft of this issue assumed one kind of container replacement. There are two, they are indistinguishable from inside without a sensor, and they have opposite consequences for any sensor built from a local file. 1. The 23:30 boot destroyed everything. A session was demonstrably alive at 23:28 — this issue's own last edit — and nothing it wrote survived anywhere writable. 2. The 23:45 boot preserved the disk. Same appearance from inside, opposite consequences. A heartbeat file under 3. The surviving hold directory was empty. 4. The last 182 seconds of writes did not survive, and this falsifies the predicate this issue shipped with. Every surviving pre-boot write stops at 23:42:15 — the MCP logs of four different servers, Serena's log, So the first draft's predicate — The heartbeat question, stated so it can be settled rather than argued. Two different things are called a heartbeat here and only one is cheap:
The first is a prerequisite for deciding the second. Ship the sensor, read it, then decide. The activity-versus-existence question — every wait in this repo that survives does I/O, and this one does not — is CLOUD-500, deliberately blocked on this issue for that reason. What the sensor must distinguish, and how The hold records why it stopped, not merely when it last ran — absence of an intentional-exit record is the signal, and absence is the one reading robust to losing the tail. Two line kinds in one appended file:
The residual error is bounded and in the conservative direction: a hold released inside the lost-write window reads as "was live". The residue probe of the first draft does not work either, and is replaced. It separated the last two rows by whether prior-container residue exists, naming Refinement — Ready
Acceptance
Measured in the session that also filed CLOUD-488; the plan whose approval button was destroyed was the one grooming CLOUD-427. The 23:30–23:46 measurements and the empty-hold-directory artifact were added in the grooming session, which was itself restarted twice while doing it. |
YAML opens a comment at an unquoted ` #`, so
run-name: fast-forward #${{ github.event.issue.number }} @${{ ... }}
parsed to the bare string `fast-forward` and both interpolations were discarded
before GitHub saw them. Measured: all 30 `fast-forward.yml` runs with
event=issue_comment between 2026-08-12T21:47Z and 2026-08-13T00:29Z reported a
display_title equal to the workflow NAME, including all four that concluded
success. run-name had been on main for six hours before the earliest of those,
so it was not deployment lag.
What it cost: land builds FF_KEY and selects .display_title == env.FF_KEY, so
that selection was empty on every lap, for every PR, always. An empty answer
resets answer_unknowns rather than charging it, so max_unknowns can never trip
on that path and the only remaining exits are the PR leaving OPEN and main
moving. A fast-forward that succeeds still lands through the fallback; one that
concludes failure or skipped is invisible and the lap polls forever holding the
fleet-wide landing lease. That is CLOUD-235's original defect restored for the
refusal case, with the success case masking it.
Nothing could have caught it. A comment is legal YAML: actionlint and zizmor
both pass, and review reads the line as the thing it was meant to be.
The gate is anchored rather than counted, and the difference is measured. The
obvious predicate - compare the raw `${{` count against the count surviving a
YAML parse - flags 4 of this repo's 20 workflows and 3 are prose comments
legitimately discussing `${{ }}`. A gate that is 75% false positives gets
switched off. Requiring a `key:` before the `#` and an interpolation after it,
with whole-line comments and quoted values skipped, flags exactly the one real
defect and nothing else.
The quoted-value exclusion was found by the gate refusing its own repair on the
first run, which is the cheapest way to find it.
Refs: CLOUD-507
Every path ONTO the board is gated except the one that creates rows.
`claim-check` refuses an issue already claimed, `claim-guard` an edit with
no claim receipt, `issue-guard` a PR naming no issue, `ready-lint` an
unrefined block, `graph-check` an incoherent frontier. Filing has none,
and the cost is not a redundant row: measured 2026-08-13, a session
blocked behind a starved landing lease spent ~45 minutes re-deriving a
cause that was already filed and searchable, reported twice to a human
that no answer was available, and filed a duplicate of an issue two
searches return in seconds.
The obvious gate is forbidden. Scoring a candidate against open issues
and refusing above a threshold is a model verdict, and a gate resolves to
a command and an exit code over an object it decides. So this does not
judge similarity and cannot tell you whether what you are filing is a
duplicate — it refuses to let the question go unasked.
The shape is `claim-check`'s, borrowed wholesale: no tracker credential
exists in a hook, so the agent searches, `issue-search-check` records
that the search happened, and `issue-search-guard` demands the record.
The receipt is keyed by branch, like the claim receipt and for the same
reason — a search attests to a decision about what to file, and every
filing on that branch continues to serve it.
A search returning nothing still mints the receipt. That is the
load-bearing case, not an edge one: zero hits is the commonest honest
outcome for a genuinely new finding, and refusing it would make the gate
punish the behaviour it exists to produce.
The matcher is suffix-anchored (`.*save_issue`), never prefixed on
`mcp__Linear__` — CLOUD-178 measured the same connector under three live
spellings, and a rule naming one matches none of the others silently. The
guard body matches on the suffix too, so a prefix-anchored matcher
degrades to "never fires" rather than to "fires on the wrong tool".
Updates are never gated; the create-vs-update discriminator is
`finding-sink-check`'s proven `has("id")` test. Both gates carry a
`#MUTANT` row and both are added to `MUTANT_GATES`; the suite is split in
two because `mutant` derives a suite from the gate's own name.
Refs: CLOUD-505
CLOUD-456 names two independent limits on the same read, and only the first was closed. The key (`display_title == $FF_KEY`) says WHICH run is this lap's; the depth says whether this lap's run is in the page at all. A keyed filter over a window that has already rolled past the run returns empty, which the poll reads as "not answered yet" — byte-identical to a silent bot, which is the reading CLOUD-399 recorded as "the bot is slow" while the bot was in fact answering inside 23 seconds. `per_page=100` was still a literal. At the measured 13 runs/minute one page is ~7.7 minutes of history and a lap routinely outlives it. So the verdict read now pages until a page comes back short, which terminates because `created>=SINCE` bounds the set server-side: the pages walk a finite window, not all history. The depth is derived from the window, which is what the acceptance asks for. The 20-page ceiling is a runaway backstop and nothing else — 2000 runs is ~2.5 hours at the measured rate, far outside any lap's `SINCE`, so reaching it means the `created` fence stopped being honoured rather than that the window is that deep. Two cases in `tests/land.bats`, the pair §7 asks for: this lap's run on page two behind 100 strangers is still read as a refusal, and a walk that meets a short page stops there rather than reaching back to an earlier lap's own run — the livelock the `SINCE` stamp exists to prevent. A `#MUTANT` row collapses the short-page break into an unconditional one, so the depth row is proven able to fail (CLOUD-418); an unmatched filter and an unanswered bot are otherwise the same bytes, which is how this survived a landed fix for a day. The structural sensor gains the other half of that coupling: the `run-name:` line must carry BOTH interpolations, not merely exist. A line that survives a `grep -c '^run-name:'` while its value is truncated to the bare workflow name is exactly what CLOUD-507 was. Refs: CLOUD-456
e42c45d to
49b3414
Compare
All 15 rows of the two new suites passed `verify` here and failed CI with `fatal: empty ident name`. A developer machine carries a global git identity and a runner does not, so `git commit` inside the fixture repo worked locally and could not work there — the verify/CI disagreement `land` refuses to land through, and it cost a full CI run to discover. Every other fixture suite in this tree already sets the identity per fixture (`claim-guard.bats`, `claim-check.bats`, `contract-drift.bats`, and the `-c user.email=` spelling in `claimed-keys.bats` and `deferral-check.bats`); these two were the only ones inheriting it. Reproduced with `GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null`, which is green at 15/15 with the identity set and is what the CI log shows red without it. Refs: CLOUD-505
|
|
/fast-forward |



Three commits, three issues, one landing lease. Each commit stands alone and
carries its own
Refs:trailer; fast-forward landing preserves their SHAs, sothis is one PR rather than three racing for the lease.
ci(fast-forward)— CLOUD-507run-name: fast-forward #${{ ... }}had an unquoted#, which opens a YAMLcomment. The value GitHub received was the bare string
fast-forward— measuredacross all 30
issue_commentruns in a 2h42m window, one distinct value, allfour successes included.
land'sselect(.display_title == env.FF_KEY)hastherefore been empty on every lap, for every PR, since it landed in
ac212b2.An empty answer reads as "not answered yet", so a fast-forward that concluded
failureleft the lap polling indefinitely while holding the fleet-wide lease.Quoted, and
ci-local-paritygains the property that would have caught it: akey's value carrying an unquoted
#that swallows a${{. The naive form(raw vs. parsed interpolation count) flags 4 files with 3 false positives; the
tightened predicate flags exactly
fast-forward.yml:17and nothing else acrossall 20 workflows. That measurement is in the comment beside the pattern.
Four rows in
tests/ci-local-parity.bats: the refusal, plus threefalse-positive guards (a whole-line comment mentioning
${{ }}, a quoted valuecontaining both, and a trailing
#with no interpolation after it).fix(hooks)— CLOUD-505Every path onto the board is gated except the one that creates rows.
claim-check,claim-guard,issue-guard,ready-lint,graph-check— andfiling, nothing. Measured cost on 2026-08-13: a session blocked behind a starved
lease spent ~45 minutes re-deriving a cause that was already filed, reported
twice to a human that no answer was available, and opened a duplicate of an
issue two searches return in seconds.
issue-search-checkreads alist_issuespayload on stdin and mints abranch-keyed receipt;
issue-search-guarddenies asave_issuecreate thathas none. It contains no similarity judgement — that would be the model verdict
non-negotiable 3 forbids — so it cannot tell you whether you are filing a
duplicate, only refuse to let the question go unasked. A search returning zero
hits still mints the receipt; that is the load-bearing case, since zero hits is
the honest outcome for a genuinely new finding.
The matcher is suffix-anchored (
.*save_issue) and the guard body matches onthe suffix too, so a prefix-anchored matcher degrades to "never fires" rather
than "fires on the wrong tool" (CLOUD-178: three live connector spellings).
Updates are never gated, via
finding-sink-check's provenhas("id")test.fix(land)— CLOUD-456CLOUD-456 names two independent limits on the same read and only the key was
closed.
per_page=100was still a literal: at the measured 13 runs/minute onepage is ~7.7 minutes and a lap routinely outlives it, so this lap's own run can
fall off the page — which reads as a silent bot, byte-identical to the failure
above.
The verdict read now pages until a page comes back short. That terminates
because
created>=SINCEbounds the set server-side, so the depth is derivedfrom the window rather than from a page size. The 20-page ceiling is a runaway
backstop: 2000 runs is ~2.5 hours at the measured rate, so reaching it means the
createdfence stopped being honoured.Shown able to fail
mutantis green at 12/12 declared mutations, including a newverdict-first-page-onlyrow that collapses the short-page break and reddensthe depth case — an unmatched filter and an unanswered bot are otherwise the
same bytes, which is how CLOUD-507 survived a landed fix for a day. Both new
gates are added to
MUTANT_GATES; the suite is split intoissue-search-check.bats/issue-search-guard.batsbecausemutantderives asuite from the gate's own name.
mise run verifygreen on this HEAD, rebased onorigin/main. Suite at1469/1469.
Note
CLOUD-507's own effect is not observable during this PR's landing: an
issue_commentrun uses the default branch's workflow, sodisplay_titlestays bare until this merges. Commit 3's tests are offline fixtures precisely so
they do not depend on that.
Generated by Claude Code