Raised by the check-in session while reviewing desk#92, which adds a third dispatch door so a session without .github attached can claim (desk#91). That PR states, honestly, that its concurrency group cannot serialize against this repo's copy. The measurement below says the existing two doors never serialized against each other either, so the PR widens a hole rather than opening one — and the repair belongs here, not there.
Not claimed. Recorded so the next reader does not re-derive it.
Measured, from the files
| door |
runs in |
concurrency.group |
group for .github-private#929 |
claim-ticket.yml (door 1) |
bounded-systems/.github |
claim-${{ inputs.repo }}-${{ inputs.issue }} |
claim-.github-private-929 |
_claim.yml via claim.yml (door 2) |
bounded-systems/.github-private |
claim-${{ github.repository }}-${{ inputs.issue }} |
claim-bounded-systems/.github-private-929 |
Two independent reasons they cannot collide: GitHub scopes concurrency groups per repository, and the two expressions do not even produce the same string — one keys on the target repo, the other on the calling repo.
CLAUDE.md describes door 2 as the route for .github-private issues when door 1 is unreachable. Both can therefore target one issue, and nothing has ever ordered them.
Why the backstop does not catch it
claim-ticket.yml's read-back is:
after="$(gh api "$api")"
if [ "$(jq -r '[.labels[].name] | contains(["claimed"])' <<<"$after")" != "true" ]; then
That asserts a claim recorded, not that this run's claim did, and the label add is idempotent. So in a genuine race both runs read the issue as unclaimed, both add the label, both post a claim comment, both read back a present label, and both exit green. Two claimants each hold a truthful-looking record.
Why it has not bitten
Since #264 every mechanized claim needs a distinct passkey approval, and the keeper renders {action, repo, issue, claimant} to the approver. A cross-door race needs one human to approve two ceremonies for the same issue inside the read-write window. The human is the de facto serializer, which is not a mechanism and is not what the ladder claims.
Proposed repair, cheaper than a lease
desk#92 concludes "the repair is one door with a lease behind it." A lease is the right end state and is not needed for this. Two steps, both small, both in this repo:
1. Make the read-back assert sole ownership. After writing, re-read the issue's comments, select those carrying the <!-- claim-ticket --> marker, and order them by created_at then comment id. If the earliest is not this run's claimant, delete this run's comment, remove the label if this run added it, and fail naming the holder. Both racing runs compute the same winner from the same record, so the loser stands down deterministically. This works across doors and across repositories, because it is a property of the issue's own record rather than of a concurrency group — which is exactly what the table above shows the groups never were.
2. Extract the cross-repo claim step into _claim-ticket.yml (workflow_call), and make every dispatch door a thin caller. Today the record logic exists twice: inline in claim-ticket.yml, and again in _claim.yml. desk#92 copies the first byte-identically, making three. CLAUDE.md already tells .github-private's door "fix claim behaviour there, never here", and claim.yml already is a thin caller — this makes the same true of the ticket door, so step 1 lands in one file instead of three.
Do 1 first; it is the correctness fix and it is a dozen lines. 2 is what stops the next door re-copying it.
Not in scope
Whether desk#92 should merge. It should, on its own terms: it inherits this gap rather than creating it, and holding a cold-start door for a pre-existing race in a different repo would be the wrong trade. Its caveat text is accurate about the mechanism and wrong only in implying a single-door world preceded it — corrected on that PR.
Raised by the check-in session while reviewing
desk#92, which adds a third dispatch door so a session without.githubattached can claim (desk#91). That PR states, honestly, that itsconcurrencygroup cannot serialize against this repo's copy. The measurement below says the existing two doors never serialized against each other either, so the PR widens a hole rather than opening one — and the repair belongs here, not there.Not claimed. Recorded so the next reader does not re-derive it.
Measured, from the files
concurrency.group.github-private#929claim-ticket.yml(door 1)bounded-systems/.githubclaim-${{ inputs.repo }}-${{ inputs.issue }}claim-.github-private-929_claim.ymlviaclaim.yml(door 2)bounded-systems/.github-privateclaim-${{ github.repository }}-${{ inputs.issue }}claim-bounded-systems/.github-private-929Two independent reasons they cannot collide: GitHub scopes concurrency groups per repository, and the two expressions do not even produce the same string — one keys on the target repo, the other on the calling repo.
CLAUDE.mddescribes door 2 as the route for.github-privateissues when door 1 is unreachable. Both can therefore target one issue, and nothing has ever ordered them.Why the backstop does not catch it
claim-ticket.yml's read-back is:That asserts a claim recorded, not that this run's claim did, and the label add is idempotent. So in a genuine race both runs read the issue as unclaimed, both add the label, both post a claim comment, both read back a present label, and both exit green. Two claimants each hold a truthful-looking record.
Why it has not bitten
Since #264 every mechanized claim needs a distinct passkey approval, and the keeper renders
{action, repo, issue, claimant}to the approver. A cross-door race needs one human to approve two ceremonies for the same issue inside the read-write window. The human is the de facto serializer, which is not a mechanism and is not what the ladder claims.Proposed repair, cheaper than a lease
desk#92concludes "the repair is one door with a lease behind it." A lease is the right end state and is not needed for this. Two steps, both small, both in this repo:1. Make the read-back assert sole ownership. After writing, re-read the issue's comments, select those carrying the
<!-- claim-ticket -->marker, and order them bycreated_atthen comment id. If the earliest is not this run's claimant, delete this run's comment, remove the label if this run added it, and fail naming the holder. Both racing runs compute the same winner from the same record, so the loser stands down deterministically. This works across doors and across repositories, because it is a property of the issue's own record rather than of a concurrency group — which is exactly what the table above shows the groups never were.2. Extract the cross-repo claim step into
_claim-ticket.yml(workflow_call), and make every dispatch door a thin caller. Today the record logic exists twice: inline inclaim-ticket.yml, and again in_claim.yml.desk#92copies the first byte-identically, making three.CLAUDE.mdalready tells.github-private's door "fix claim behaviour there, never here", andclaim.ymlalready is a thin caller — this makes the same true of the ticket door, so step 1 lands in one file instead of three.Do 1 first; it is the correctness fix and it is a dozen lines. 2 is what stops the next door re-copying it.
Not in scope
Whether
desk#92should merge. It should, on its own terms: it inherits this gap rather than creating it, and holding a cold-start door for a pre-existing race in a different repo would be the wrong trade. Its caveat text is accurate about the mechanism and wrong only in implying a single-door world preceded it — corrected on that PR.