Repository navigation
fix(tasks): read a cancelled required check as no verdict, not as red - #302
Conversation
CLOUD-363 A `cancelled` required check is read as red, so a run superseded by `land`'s own ready/push race wedges the branch permanently
Why Measured on PR #293 (CLOUD-346), 2026-08-11. The run on the SHA that would land was the one cancelled; the run on the stale SHA survived. The red is not a verdict. A cancelled run did not judge anything — it is the absence of an answer, exactly like the draft-era $2 == "skipped" { skipped = ...; next }
$2 == "success" || $2 == "neutral" { graded++; next }
{ graded++; bad = bad sep4 $3 " " $2; sep4 = ", " } # <- cancelled lands hereAnd that wedges the branch, which is the part that makes this urgent. The two rules compose into a trap with no exit:
Observed exactly that: two consecutive Ready
Test obligation
Commit type / bump: Blockers: none. It sits on top of CLOUD-346's Acceptance
|
955ba46 to
8029893
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit 37bf48d4f645e57937455ef8ca4d791a4859bf4a (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:58:45 2026 +0000
feat(spec): emit the derived read-only allowlist beside the tree
`spec::read_only_allowlist` derives §5's agent allowlist from the same walk
that builds the emitted tree, so an unclassified command cannot leak in — but
it had no caller outside this crate's own tests. A derivation nothing emits is
one every consumer has to re-implement, and this filter's failure mode is
advertising a write-bearing verb as agent-safe: `enforce` may run user-supplied
commands and `hook` adjudicates someone else's write, and both stay off the
list only because the filter is `effect == read`.
So emit it. `SpecDocument` flattens the command tree at the document root and
adds `read_only_allowlist` beside it, which keeps every root key a consumer
already reads (`path`, `subcommands`, …) exactly where it was.
The pinning test asserts the emitted key IS the `effect == read` filter over
the emitted tree, recomputed from the same document — not a second copy of the
path list, which `allowlist_is_exactly_the_read_commands` already holds.
Refs: CLOUD-217Pull request ( commit 8029893b96cd9ad620d2b308bde58e8d28c82041 (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * 37bf48d4f645e57937455ef8ca4d791a4859bf4a feat(spec): emit the derived read-only allowlist beside the tree
| * 8029893b96cd9ad620d2b308bde58e8d28c82041 fix(tasks): read a cancelled required check as no verdict, not as red
|/
* dc3dcc58c52ae6bc946450ada2ed463225c2cbc2 ci(memory): correct the fan-out cost model and the plan-destination rule
commit dc3dcc58c52ae6bc946450ada2ed463225c2cbc2
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:57:18 2026 +0000
ci(memory): correct the fan-out cost model and the plan-destination rule
Two things this file said that optimise against the wrong cost.
The cost model. It named a rising re-verify rate as the stop signal for
parallelism. `land` laps unattended — a fast-forward refusal rebases and
re-verifies with no model turn — so a moved base spends CPU and wall-clock,
both free here, and zero tokens. Re-verifying IS the loop working. What
actually costs is narrower: a rebase CONFLICT needs a human, a CI run `main`
voids burns metered minutes, and tokens are spent only when a session returns
to the model. Each has its own control, and they are now tabulated. The
objective is pace of landed work per token, not collision avoidance:
coordination between sessions is impossible by construction, so collisions are
designed for in the CSMA/CD sense, and the target is a saturated queue — every
merge followed by a sibling already rebased, verified and ready behind it.
The plan destination. Telling a child to post its plan as a Linear comment was
carried over from a planning-only fleet that had no approval channel, where a
conclusion not written down died with the session. Under plan mode the approval
UI is the destination and the work lands as commits and PRs, so the instruction
is now a second authority that goes stale the moment the plan changes under
review. It survives only for research and decision tickets, whose output is a
conclusion rather than an artifact.
The admission-control gap this exposes — nothing rations the transition out of
draft, so N sessions buy N confirming runs for one merge slot — is CLOUD-369,
not prose here.
Refs: CLOUD-367Rebase locally, and then force push to |
8029893 to
f14116c
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit ec3639a5f87a043c350ff85159db768a3fe3cfcf (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:51:35 2026 +0000
fix(session-start): run doctor inside the hook's synchronous window
`mise install` returning is not "the toolchain is settled". doctor provisions
what mise does not own — the rustup cross targets above all — and left outside
the SessionStart window it runs for the first time inside the first
`mise run verify`, concurrently with whatever the sandbox is still laying down.
Measured on a cold container 2026-08-07: that first verify died in doctor with a
`detected conflict` rollback on rust-std-aarch64-apple-darwin, and a second
doctor, nothing changed but time, exited 0. The message names cross-compilation,
so it costs the reader a debugging session on a machine where nothing is broken.
This is not a retry. It does not make the race survivable, it empties the window
the race needs: by the time any task runs, the targets are already installed.
CLOUD-220's per-toolchain mutex in target-ensure serializes concurrent writers
and still stands — it covers the warm case (the verify graph racing itself,
CLOUD-201), this covers the cold one. doctor itself is unchanged.
The step goes through the hook's existing `step` helper, so it inherits the
loud-failure contract, and runs after mise install (its rustup half needs the
provisioned toolchain) and before container-preflight (a halt, which wants
provisioning finished). tests/session-start.bats records every mise invocation
through its stub and asserts that ordering, rather than grepping the source;
doctor joins container-preflight in being neutralised there, because its rustup
half reaches the network and test:bats already depends on `doctor --no-targets`.
Refs: CLOUD-218Pull request ( commit f14116c58487d8acf1f1faec969e0336ceb63365 (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * f14116c58487d8acf1f1faec969e0336ceb63365 fix(tasks): read a cancelled required check as no verdict, not as red
| * ec3639a5f87a043c350ff85159db768a3fe3cfcf fix(session-start): run doctor inside the hook's synchronous window
|/
* 37bf48d4f645e57937455ef8ca4d791a4859bf4a feat(spec): emit the derived read-only allowlist beside the tree
commit 37bf48d4f645e57937455ef8ca4d791a4859bf4a
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:58:45 2026 +0000
feat(spec): emit the derived read-only allowlist beside the tree
`spec::read_only_allowlist` derives §5's agent allowlist from the same walk
that builds the emitted tree, so an unclassified command cannot leak in — but
it had no caller outside this crate's own tests. A derivation nothing emits is
one every consumer has to re-implement, and this filter's failure mode is
advertising a write-bearing verb as agent-safe: `enforce` may run user-supplied
commands and `hook` adjudicates someone else's write, and both stay off the
list only because the filter is `effect == read`.
So emit it. `SpecDocument` flattens the command tree at the document root and
adds `read_only_allowlist` beside it, which keeps every root key a consumer
already reads (`path`, `subcommands`, …) exactly where it was.
The pinning test asserts the emitted key IS the `effect == read` filter over
the emitted tree, recomputed from the same document — not a second copy of the
path list, which `allowlist_is_exactly_the_read_commands` already holds.
Refs: CLOUD-217Rebase locally, and then force push to |
f14116c to
ff37991
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit f34f99a3a2c842b064087c3deb6e2476f412ed29 (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:59:53 2026 +0000
test(findings): hold the no-escalation law over the bytes, not over a struct
CLOUD-80's residual acceptance clause — "escalation on re-touch is absent" —
was carried by an in-memory test that drives `FindingRecord::upsert` on a
struct. That test cannot see either way a stored tier could actually move: a
write path that re-derives it, and a later scan that re-rates the rule. Both
need a real store dir.
Two store-level tests, asserted over the record file:
- `an_nth_observation_never_re_tiers_the_stored_record` runs the same identity
through `record` five times, total over `RuleSeverity::ALL` so every
`AdvisoryTier` is exercised, and compares the persisted `"tier"` line
byte-for-byte against the first observation's — while asserting the
occurrence count did move, so it cannot pass on a store that recorded
nothing.
- `a_re_rated_rule_never_re_tiers_a_settled_finding` records one identity at
`warn`, then again at `deny`, and pins that the settled record keeps tier
`caution` and severity `warn`. Severity is not an identity input, so the
re-rating routes to the same record; `record` reusing it rather than
refreshing from the rule now firing was documented and untested.
Negative control run before landing: re-deriving the tier in `record`'s update
path, plus a count-keyed escalation, fails both new tests and leaves the
in-memory sibling green.
Refs: CLOUD-80Pull request ( commit ff37991e419270d80475f83f68eb843322566c62 (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * ff37991e419270d80475f83f68eb843322566c62 fix(tasks): read a cancelled required check as no verdict, not as red
| * f34f99a3a2c842b064087c3deb6e2476f412ed29 test(findings): hold the no-escalation law over the bytes, not over a struct
|/
* ec3639a5f87a043c350ff85159db768a3fe3cfcf fix(session-start): run doctor inside the hook's synchronous window
commit ec3639a5f87a043c350ff85159db768a3fe3cfcf
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:51:35 2026 +0000
fix(session-start): run doctor inside the hook's synchronous window
`mise install` returning is not "the toolchain is settled". doctor provisions
what mise does not own — the rustup cross targets above all — and left outside
the SessionStart window it runs for the first time inside the first
`mise run verify`, concurrently with whatever the sandbox is still laying down.
Measured on a cold container 2026-08-07: that first verify died in doctor with a
`detected conflict` rollback on rust-std-aarch64-apple-darwin, and a second
doctor, nothing changed but time, exited 0. The message names cross-compilation,
so it costs the reader a debugging session on a machine where nothing is broken.
This is not a retry. It does not make the race survivable, it empties the window
the race needs: by the time any task runs, the targets are already installed.
CLOUD-220's per-toolchain mutex in target-ensure serializes concurrent writers
and still stands — it covers the warm case (the verify graph racing itself,
CLOUD-201), this covers the cold one. doctor itself is unchanged.
The step goes through the hook's existing `step` helper, so it inherits the
loud-failure contract, and runs after mise install (its rustup half needs the
provisioned toolchain) and before container-preflight (a halt, which wants
provisioning finished). tests/session-start.bats records every mise invocation
through its stub and asserts that ordering, rather than grepping the source;
doctor joins container-preflight in being neutralised there, because its rustup
half reaches the network and test:bats already depends on `doctor --no-targets`.
Refs: CLOUD-218Rebase locally, and then force push to |
111a7c7 to
ba7baac
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit 42ef66342352cf2219fd94ab2503dc228e1dd8b5 (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:09:46 2026 +0000
test(session): compare the whole store across a restart, not a subset
The predicate is that a kill-and-restart preserves the findings store
byte-for-byte, and the assertion was over the fields the test helper
happens to parse — fingerprint, rule and instance counts. It would not
have noticed the tier, the presentation or a disposition changing across
the restart, which are exactly the fields no verb can vary yet and so the
ones nothing else is watching.
Refs: CLOUD-83Pull request ( commit ba7baac290223a5b96837dcb58457248557785df (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * ba7baac290223a5b96837dcb58457248557785df fix(tasks): read a cancelled required check as no verdict, not as red
| * 42ef66342352cf2219fd94ab2503dc228e1dd8b5 test(session): compare the whole store across a restart, not a subset
| * 497f6d5b98b533c525d6da0dc678604082dfa83a test(session): pin what a warm fork must keep
| * e7165259b0afb55ca82b7d23a72b18bb6987cb88 feat(state): record where a session left the journal
| * 708d12c6e7e58b4653f18373c94798ec9eb33233 feat(session): give a warm fork somewhere to resume from
|/
* 78d313aea99ff609bf6e24360538bd8ba4543354 fix(lock-complete): name lock-check as the remedy, not the raw regeneration command
commit 78d313aea99ff609bf6e24360538bd8ba4543354
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:56:11 2026 +0000
fix(lock-complete): name lock-check as the remedy, not the raw regeneration command
The unlocked-tool pointer spelled out the regeneration command, which put the
literal into an executable line — where tests/lock-complete.bats's "makes no
network call" assertion reads it as this gate fetching. That assertion is a
coarse string match on purpose and is worth keeping coarse, so the message
moves instead.
`mise run lock-check` is also the better pointer on its own terms: it is the
single sanctioned lockfile writer, since `[settings] lockfile = false` denies
the write to everything else (CLOUD-223).
Refs: CLOUD-333Rebase locally, and then force push to |
3d5f93a to
47c271d
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit 3fde7a81593c6a60d838b6006f8db6a638b4738f (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:30:27 2026 +0000
fix(bench): keep the workflow's summary block off shellcheck's SC2016
A backtick pair inside single quotes reads as legacy command substitution
to shellcheck, which actionlint runs over every `run:` block, so the
summary line failed the gate. Double-quoted with escaped backticks keeps
the markdown code span and says nothing shellcheck has to guess at.
Prettier's table realignment in README rides along — hk staged it.
Refs: CLOUD-207Pull request ( commit 47c271db03a0890617889513b8213eaa7a5b4ee7 (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * 47c271db03a0890617889513b8213eaa7a5b4ee7 fix(tasks): read a cancelled required check as no verdict, not as red
| * 3fde7a81593c6a60d838b6006f8db6a638b4738f fix(bench): keep the workflow's summary block off shellcheck's SC2016
| * 8854916d55b1126fcae162c601404edfc5cc6f9e feat(bench): measure the invocation cost, publish it, and gate it
|/
* 040fa566b81b0c89387ac7bdb833e0e43d028e1c docs(resolve): mark §5's max_effect as specified, not implemented
commit 040fa566b81b0c89387ac7bdb833e0e43d028e1c
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:24:16 2026 +0000
docs(resolve): mark §5's max_effect as specified, not implemented
Two module headers asserted `max_effect` as an invariant already in the tree:
`resolve.rs` said the raise-only override rule "extends §5's `max_effect`
invariant to the config layer", and `verbs.rs` said sharing the effect
vocabulary keeps "the raise-only `max_effect` rule" meaning one thing across
the tool. Neither is true today. `max_effect` is per-flag effect annotations
plus a monotone maximum over them; `Effect` is declared per command and carries
no ordering, so there is nothing to take a maximum of.
That is the CLOUD-198 class, and `resolve.rs` was the costly instance: it
presented this layer's raise-only rule as a corollary of a mechanism that
exists, so a reader checking whether config-layer monotonicity was load-bearing
on its own found a sentence telling them it was not.
Both now name the invariant as specified-not-implemented and point at CLOUD-27,
which is where the implementation rides. No test: the Ready block puts the test
obligation on the implementation path, and a doc comment stating a gap has
nothing to assert until the gap closes.
Refs: CLOUD-217Rebase locally, and then force push to |
47c271d to
bfd8395
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit a2ed37d212a129bfc423cf34a378d890ce19a0d2 (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:23:26 2026 +0000
fix(lint): add the lint rows to the two committed surface lists, and fix the census's clippy denial
`verify` found three things local iteration had not:
- `spec.rs` pins the emitted row set and the derived read-only allowlist as two
committed lists. Both are hand-maintained on purpose — a verb added, renamed
or re-parented has to be stated — so `lint`/`lint brief` join them, the noun
with its kind because the whole subtree is `read`.
- The census's positional lookup used `map(..).unwrap_or_else(panic!)`, which is
`clippy::map_unwrap_or`; a `let ... else` says the same thing.
- rustfmt over the new module, the surface row and the tests.
Refs: CLOUD-84Pull request ( commit bfd83951bb2748a2b88055912d578c6a95b4546e (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * bfd83951bb2748a2b88055912d578c6a95b4546e fix(tasks): read a cancelled required check as no verdict, not as red
| * a2ed37d212a129bfc423cf34a378d890ce19a0d2 fix(lint): add the lint rows to the two committed surface lists, and fix the census's clippy denial
| * bb83902973a8652f1db3dbcc68820996559d5310 feat(lint): gate a delegation brief on the facts that do not inherit
|/
* 3fde7a81593c6a60d838b6006f8db6a638b4738f fix(bench): keep the workflow's summary block off shellcheck's SC2016
commit 3fde7a81593c6a60d838b6006f8db6a638b4738f
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:30:27 2026 +0000
fix(bench): keep the workflow's summary block off shellcheck's SC2016
A backtick pair inside single quotes reads as legacy command substitution
to shellcheck, which actionlint runs over every `run:` block, so the
summary line failed the gate. Double-quoted with escaped backticks keeps
the markdown code span and says nothing shellcheck has to guess at.
Prettier's table realignment in README rides along — hk staged it.
Refs: CLOUD-207Rebase locally, and then force push to |
ec49329 to
392b7db
Compare
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit 6947232b13bc17174986a0e3bfe528d848617519 (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:57:59 2026 +0000
perf(hk): glob batten-check and macos-link-check, and gate the glob's honesty
`hk check` ran three steps unconditionally, because a step with no `glob` is
never skipped. `batten-check` is `cargo run -p batten -- check`, so every
commit paid a debug build whatever it touched: 9.47s cold from a fresh clone
(CLOUD-212 run 1, D8) against 0.16s for the cheapest gate in the same run, and
133-141ms warm, forever.
The glob is the union of what `batten check` ACTUALLY reads, which is not the
two things the obvious reading gives. Read at run_check_with in
crates/batten/src/lib.rs, it evaluates every `[[rule]]` over that rule's own
glob — and those reach outside crates/, to mise.toml, .github/workflows/*.yml
and tests/*.bats — plus `budget::measure_all`, because a declared budget is a
gate under `check` and not only under `policy budget` (CLOUD-50). That last one
makes AGENTS.md and .serena/project.yml genuine inputs, so "a Markdown-only
change cannot change this verdict" is false for the most-edited Markdown file
in the repo, and a glob written from the obvious reading would have switched
the gate off for it. Cargo.toml and Cargo.lock are in because the task uses
`cargo run` deliberately, to judge the engine and the config as the pair that
ships.
`macos-link-check` globs the manifests alone: its predicate is `cargo metadata
--filter-platform` over the resolved graph, and nothing else can move that
graph. `no-docs-tree` deliberately keeps no glob — its input is the whole
index, including a docs/ path an earlier commit left behind, and at ~95ms that
is affordable.
Deriving batten-check's list from batten.toml by hand makes hk.pkl a second
authority over a set the config already defines, and a second authority narrows
silently: add a rule whose glob names a new path and the step stops running for
commits that touch only it, with nothing going red. So the glob ships with its
mechanism (non-negotiable 2). `mise run batten-glob-check` is a pure function
of the two committed files — every rule glob and every declared budget path
must be covered verbatim or subsumed by a `P/**` entry, and a config it can
parse nothing out of is exit 2 rather than a vacuous pass.
Selection is asserted as data, never as a timing: `hk check --plan --json`
prints each step's status without running it, and tests/hk-selection.bats pins
both directions over the repository's own hk.pkl — README.md skips
batten-check, AGENTS.md does not.
Refs: CLOUD-224Pull request ( commit 392b7db6723d7b0a56c7d7d45893d0040d5cc00f (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * 392b7db6723d7b0a56c7d7d45893d0040d5cc00f fix(tasks): read a cancelled required check as no verdict, not as red
| * 6947232b13bc17174986a0e3bfe528d848617519 perf(hk): glob batten-check and macos-link-check, and gate the glob's honesty
|/
* 6569aeb303e942642dd22feab96f221c67f7027e feat(rules)!: rename the command kind's `run` column to `check`, reserve `fix`
commit 6569aeb303e942642dd22feab96f221c67f7027e
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:02:59 2026 +0000
feat(rules)!: rename the command kind's `run` column to `check`, reserve `fix`
House style §9 specifies the `command` rule kind as a check/fix duality: an
inspection-only `check` is the gate — the only side enforcement ever runs — and
an optional mutating `fix` repairs what it condemned. The shipped `Rule` carried
one `run` column, which does not express the split.
§2 declares no back-compatibility surface and no deprecation aliases, so every
config written against `run` becomes a breaking migration later. Today the key
appears in no shipped config at all — only fixtures, tests and the README — so
the rename costs nothing. It will not stay that way.
`fix` parses and is executed by nothing: serialised fix execution is a
capability this engine does not have. `enforce` refuses a row declaring one
rather than running the check side and silently ignoring the repair, which would
report green over work nobody did. Reserving the key now means an author who
writes it is writing the final spelling.
`run` survives as a refusal-only field, the shape `OverrideConfig::
min_batten_version` already uses: "unknown field `run`" reads as a typo, where
this is a rename with one specific fix, and the refusal has to carry it
(CLOUD-122). It is deliberately outside `Rule::columns` — that census classifies
the columns a kind may carry, and no kind carries this one.
Both refusals are exit 1, not the exit 2 the issue's §5/§7 asked for. §7 makes 2
the policy verdict — a violation found in the repository, or a mediated call
denied — while a config naming a key or a capability this build does not have is
the config-or-usage class. §5 spells the structurally identical sibling refusal
(a rule the read-only half cannot honestly run) as exit 1, and `run_static` has
implemented it that way since CLOUD-170; the issue's §3 describes that same
landed refusal as exit 2, which is falsified by the code it describes. Emitting
2 here would also make an engine gap read to a harness as a policy deny, which
is what the structural fail-open exists to prevent.
Refs: CLOUD-215Rebase locally, and then force push to |
8bbef2c to
d1f26fe
Compare
|
/fast-forward |
|
/fast-forward |
|
Triggered from #302 (comment) by @wenzowski. Trying to fast forward Target branch ( commit 5596dafa6bdeb8e13dc02a254c387f4fbbc7aa8d (HEAD -> main, origin/main)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 19:26:47 2026 +0000
fix(issue-guard): ask the competitor PR which issue it claims, not which it mentions
CLOUD-338 split the claim derivation into `claimed-keys` because which issue a
branch CLAIMS is a narrower question than which it MENTIONS — a body cites
related issues as evidence, and conflating the two made this guard refuse its
own PR twice. That narrowing was applied to this branch and not to the other
one, so the comparison had a broad half and a narrow half.
Measured: PR #306, `docs(agents): point at the attribution decision record
(CLOUD-268)`, names CLOUD-133 in one row of an evidence table and nowhere else.
It refused CLOUD-133's own first PR. Every PR body in this repo cites related
issues — that is the house style — so the broad half fires on exactly the corpus
the repo is written to produce.
`claimed-keys` gains explicit sources so the same authority can answer about a
PR this checkout did not author: `--branch`, `--title` and `--log`, with stdin
still the body. Passing any of them switches off the git reads entirely rather
than falling back per source, because a remote PR silently answered from the
local branch would be a confident verdict about the wrong repository state.
The title joins the branch at precedence 2 as a UNION rather than a precedence
between them: for a PR you did not author the title is the other self-declaration
of what the work is, and this repo's own convention ends every title with
`(CLOUD-<n>)`. A body is not a self-declaration, which is the whole distinction.
Refs: CLOUD-378Pull request ( commit fde278b966a3ccaa8b3b4c8311f5c9fdc1b4a1fd (pull_request/wenzowski/cloud-363-a-cancelled-required-check-is-read-as-red-so-a-run)
Author: Claude <noreply@anthropic.com>
Date: Tue Aug 11 18:52:17 2026 +0000
fix(tasks): read a cancelled required check as no verdict, not as red
A cancelled run judged nothing. It is the absence of an answer, exactly
like the draft-era `skipped` CLOUD-327 taught this repo not to read as a
conclusion — but `checks-green`'s catch-all bucketed it with `failure`,
and the two rules composed into a trap with no exit.
Measured on #293: `land` readied then force-pushed, both events reached
the same `concurrency: ci-<ref>` group two seconds apart, and the run on
the SHA that would land was the one cancelled. `ci-wait` reported red,
`land` re-drafted and stopped — correctly, given "red". Re-running it
could not recover: HEAD was unchanged so the push moved nothing, the
verify receipt still held, and `graded_runs` counted `cancelled` as
graded, so the ready that would have replaced those runs was skipped.
Two consecutive invocations died on the identical stale set, neither
able to create a single new check-run. The only escape was a hand-minted
SHA, which is a manual step outside the loop `land` exists to drive.
Both halves move together or the trap survives the first fix:
* `checks-green` — `cancelled` joins `skipped` in the no-verdict set
(exit 3). The bucket now carries the conclusion beside the name, so a
stall that has two spellings can still be diagnosed:
`required check(s) with no verdict: ci cancelled, cross skipped`.
* `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as
"no graded run" and the next lap re-fires the ready. It gains
`neutral`, which `checks-green` has always graded: the same drift
with the sign reversed, buying a second run for a head that already
carried its verdict.
The precedence between the two buckets is now load-bearing and says so.
#293's set was `final failure` plus five `cancelled`; `final` is a
fan-in, so its failure was a consequence of the cancellations rather
than a judgement on the tree. Testing "no answer" before "red" is what
keeps that set recoverable, and a real failure leaves the bucket empty
and still exits 1.
This does not re-order ready/push. Making a cancellation recoverable is
the smaller, more general fix: it holds for a manual cancel and a runner
reclaim too.
Refs: CLOUD-363Can't fast forward * fde278b966a3ccaa8b3b4c8311f5c9fdc1b4a1fd fix(tasks): read a cancelled required check as no verdict, not as red
| * 5596dafa6bdeb8e13dc02a254c387f4fbbc7aa8d fix(issue-guard): ask the competitor PR which issue it claims, not which it mentions
|/
* bf94e1dfe0c7a8b15e9af1dd913266889b6e3531 docs(agents): point at the attribution decision record
commit bf94e1dfe0c7a8b15e9af1dd913266889b6e3531
Author: Alec Wenzowski <alec@wenzowski.com>
Date: Tue Aug 11 19:00:46 2026 +0000
docs(agents): point at the attribution decision record
The three commit-metadata surfaces — authorship (accountability), disclosure
(repo policy data) and provenance (records) — are fixed in a new authoritative
Linear doc beside the house style and the DoR/DoD. AGENTS.md carries the
pointer only; the budget is unchanged at 199 lines, paid for by tightening the
specs preamble, the index preamble and the scope reminder.
Refs: CLOUD-268Rebase locally, and then force push to |
|
Rebased onto The conflict was semantic, not textual. CLOUD-436 made
Both halves are mutation-proven, since a conflict resolution is exactly where one side gets silently dropped: removing Nothing about this PR's intent changed — only where its decision lives. Generated by Claude Code |
73b8a34 to
4203412
Compare
A cancelled run judged nothing. It is the absence of an answer, exactly like the draft-era `skipped` CLOUD-327 taught this repo not to read as a conclusion — but `checks-green`'s catch-all bucketed it with `failure`, and the two rules composed into a trap with no exit. Measured on #293: `land` readied then force-pushed, both events reached the same `concurrency: ci-<ref>` group two seconds apart, and the run on the SHA that would land was the one cancelled. `ci-wait` reported red, `land` re-drafted and stopped — correctly, given "red". Re-running it could not recover: HEAD was unchanged so the push moved nothing, the verify receipt still held, and `graded_runs` counted `cancelled` as graded, so the ready that would have replaced those runs was skipped. Two consecutive invocations died on the identical stale set, neither able to create a single new check-run. The only escape was a hand-minted SHA, which is a manual step outside the loop `land` exists to drive. Both halves move together or the trap survives the first fix: * `checks-green` — `cancelled` joins `skipped` in the no-verdict set (exit 3). The bucket now carries the conclusion beside the name, so a stall that has two spellings can still be diagnosed: `required check(s) with no verdict: ci cancelled, cross skipped`. * `land` — `graded_runs` drops `cancelled`, so a cancelled set reads as "no graded run" and the next lap re-fires the ready. It gains `neutral`, which `checks-green` has always graded: the same drift with the sign reversed, buying a second run for a head that already carried its verdict. The precedence between the two buckets is now load-bearing and says so. fan-in, so its failure was a consequence of the cancellations rather than a judgement on the tree. Testing "no answer" before "red" is what keeps that set recoverable, and a real failure leaves the bucket empty and still exits 1. This does not re-order ready/push. Making a cancellation recoverable is the smaller, more general fix: it holds for a manual cancel and a runner reclaim too. Refs: CLOUD-363
The no-verdict case went red inside a full parallel gate and passed standalone — CLOUD-426's class, in the case CLOUD-423 added. The stub watcher answered immediately, so land group-killed the verify child before it had appended its own call, and the lap that followed counted one verify where the case demands two. Whether the assertion held was decided by which of two forks won, which is exactly the shape this repo cannot afford: a red gate that reproduces nowhere teaches an agent to re-run until green. The watcher now waits for the verify it races to have registered before it answers, whichever way it is about to answer — a bounded wait on a real event, not a guessed interval, and applied to both levers rather than only the one that failed. Stressed 8x under a concurrently running full suite: zero failures. The race-deleted mutant still turns the case red, so the synchronisation bounds the timing without blunting what it detects. Refs: CLOUD-426, CLOUD-423 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X4tzyT3Q3hXo5QFEESENYP
4203412 to
4de3219
Compare
|
|
/fast-forward |
…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
…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



Fixes the wedge measured on #293 (CLOUD-363).
A cancelled run judged nothing. It is the absence of an answer, exactly like
the draft-era
skippedCLOUD-327 taught this repo not to read as a conclusion —but
checks-green's catch-all bucketed it withfailure, and the two rulescomposed into a trap with no exit.
landlap 1 readied then force-pushed (the deliberate CLOUD-254 order). Bothevents reached the same
concurrency: ci-${{ github.ref }}group two secondsapart, and the run on the SHA that would land was the one cancelled:
ci-waitreported red,landre-drafted and stopped — correct, given "red".Re-running it could not recover: HEAD was unchanged so the push moved nothing,
the verify receipt still held, and
graded_runscountedcancelledas graded,so the ready that would have replaced those runs was skipped. Two consecutive
invocations died on the identical stale set, neither able to create a single new
check-run. The only escape was a hand-minted SHA — a manual step outside the
loop
landexists to drive.Both halves, or the trap survives the first fix
mise-tasks/checks-green—cancelledjoinsskippedin the no-verdictset (exit 3); the catch-all keeps
failure/timed_out/action_required. Thebucket now carries the conclusion beside the name, so a stall with two
spellings stays diagnosable:
required check(s) with no verdict: ci cancelled, cross skipped.mise-tasks/land—graded_runsdropscancelled, so a cancelled setreads as "no graded run" and the next lap re-fires the ready. It also gains
neutral, whichchecks-greenhas always graded: the same drift with thesign reversed, buying a second run for a head that already carried its verdict.
Recovery works from both entry states. Re-drafted (what
landactually leaves):the pre-push ready block fires on
isDraft+ zero graded, and CLOUD-255 stillholds because
readied=1suppresses the--undopath. Left ready: thepost-push block fires on an unmoved ref + zero graded and
--undo/readyre-emits
ready_for_review. Either way a fresh run appears, and/commits/<sha>/check-runsdefaults tofilter=latest, so the new check-runssupersede the cancelled ones by name.
The precedence is load-bearing now, and says so
#293's set was
final failureplus fivecancelled.finalis a fan-in overthe others, so its failure was a consequence of the cancellations rather than
a judgement on the tree. Testing "no answer" before "red" is what keeps that set
recoverable; promoting red would put the branch straight back in the wedge. A
real failure leaves the no-verdict bucket empty and still exits 1 — CLOUD-363's
second acceptance clause.
Not re-ordering ready/push
The issue names that as a note rather than a task, and this PR keeps it that
way. Making a cancellation recoverable is the smaller, more general fix: it
holds for a manual cancel and a runner reclaim as well as for a concurrent push.
Tests
tests/checks-green.bats— a cancelled required check is exit 3 and names theword; a partial cancel is not redeemed by a sibling success; the measured
final failure+ fivecancelledset is exit 3, not red.tests/ci-wait.bats— the same set through the poll: it holds open ratherthan exiting 1.
tests/land.bats—head_is_all_cancelled(); a ready PR whose head is allcancelled has its ready re-fired, and the re-drafted PR the wedge leaves
behind is readied once without
--undo. Thedie/continuecoverage countsare unchanged.
Refs: CLOUD-363