Skip to content

claim-sweep: make the door a reusable, and emit the claim-activity dispatch (#290) - #291

Merged
bdelanghe merged 6 commits into
mainfrom
claude/bounded-desk-tools-3fcown
Aug 29, 2026
Merged

claim-sweep: make the door a reusable, and emit the claim-activity dispatch (#290)#291
bdelanghe merged 6 commits into
mainfrom
claude/bounded-desk-tools-3fcown

Conversation

@bdelanghe

Copy link
Copy Markdown
Contributor

Closes #290

Two changes, both consequences of the lane shipping in #286/#288 and then meeting reality.

1. The door becomes a reusable

claim-sweep.yml shipped with a cross-repo fan-out: a repo input (or *) and a token minted through the OIDC broker under a claim-sweep entry.

That entry does not exist. Measured, not inferred — dispatching the merged lane at .github-private returns:

broker refused to mint for app 'claim-sweep' (HTTP 404): unknown app: claim-sweep

So the lane worked on exactly one repository: the one it lives in. I had asserted this from reading the workflow header and then tested it, because a gate's own claim about itself is not evidence (docs/agentic-code-hygiene.md rule 3).

_claim-sweep.yml runs against the caller's github.repository with the caller's GITHUB_TOKEN. issues: write is exactly what releasing a claim uses — the same argument _claim.yml's header makes for why that door alone travels. No broker entry, no fan-in App growing issues: write, and adoption becomes a short caller instead of an infra grant.

It is also the shape this org has converged on twice. _claim.yml records what the alternative cost: "copy this file" produced copies that drifted, one of which left issues labelled claimed with no comment naming anyone — unheld, unclaimable, clearable only by hand (.github-private#652). _pr-claim.yml is the same pattern.

Behaviour moves across verbatim: the four rules in order, the standing-claim exemption, both fail-closed guards, comment-before-label ordering, and the one-request unassign with its non-fatal warning. The -d--input correction from #288 travels with the reason recorded at the call site, so nobody reintroduces it by reading _claim.yml's correct curl -d as a house idiom.

Caller drops 297 lines → 72.

2. The claim-activity sender that nothing was emitting

front-desk-feed's publish lane already accepts repository_dispatch: types: [pr-activity, claim-activity]. The type was added in front-desk-feed#6 for exactly this, and that header names the gap:

"No sender emits claim-activity yet; the claim doors are the natural one, since they are the only things that know a claim changed."

This is a claim door. After a real sweep it is the only thing in the org that knows a batch of claims stopped being claims.

Why it matters, measured three times. The feed's 35 * * * * cron is best-effort and GitHub drops slots under load — a denser cron is skipped more often:

  • 2026-08-28, PRs: six slots between 06:35 and 12:35 produced nothing; the page served 3 open for 6.5h after the last PR merged.
  • 2026-08-28, claims: seven released ~21:20–21:35Z, listed as live work — one already closed and merged — until a hand dispatch at 22:38Z.
  • 2026-08-29, today: the .github sweep released 23 at 16:59Z; the page still read 114 at 17:04Z and moved to 92 only after this session dispatched publish.yml by hand.

Fires only when something changed: never on a dry run, never when a real run released nothing — otherwise the weekly no-op cron spends a publish run republishing an identical feed. continue-on-error, because the release already happened and is already correct; a failed notification costs freshness, not truth.

Inert until provisioned, the same posture the receiving half shipped with. repository_dispatch needs contents: write on the target, which a caller's GITHUB_TOKEN does not carry cross-repo, so it wants a broker binding-plane entry bounded by an installation on that repo alone. Absent feed_dispatch_token the step prints a notice and exits 0.

The token is checked in the shell, not in the if. The secrets context's availability in a step conditional is the kind of thing that evaluates to something surprising rather than erroring, and #289 is the standing record of shipping a branch no dry run reaches. An empty env var is unambiguous on every runner.

The payload carries no issue-derived data and no repo name — every event produces the same whole-board reprojection so there is nothing to branch on, and a caller may be private while the receiver's logs are world-readable.

Evidence the behaviour is right

Run 33264394589 on the pre-refactor file, against .github:

claim-sweep: released 23, kept 0, exempted 5 claim(s) across .github.

All five standing claims printed exempt (standing claim). claims.bounded.tools moved 114 → 92 once the feed republished. Only the plumbing changed here.

Verification

  • node --test *.test.mjs256 pass (up from 254; two tests auto-cover the new workflow)
  • node --test .claude/*.test.mjs207 pass
  • node --test workflows.test.mjs95 pass (up from 93)
  • grep confirms no gh api … -d anywhere in either file

Rollout

  • standing-claim label created in both repos and applied to all six standing claims.
  • [settings] broker binding-plane entry that can mint contents: write on front-desk-feed, to activate the sender.
  • A caller in .github-private (.github-private#765) — its 69 finished claims were drained by hand in the meantime, via the REST API rather than 69 workflow runs, because that repo is ~1 day from a projected Actions budget stop (.github-private#705, #467).

Related: #286, #288, #289, .github-private#717, #765, front-desk-feed#5, desk#22.


Generated by Claude Code

claude added 6 commits August 29, 2026 16:42
`claim-ticket.yml`'s header already named this file as the fix: "Nothing
expires stale claims yet; if that starts to hurt, it is a scheduled sweep
in the board-parity family, not more logic here."

Measured 2026-08-29 against the live API: 114 claims org-wide sit on
finished work, 92 of them in this repo and `.github-private`. The claim
doors write the `claimed` label and nothing removes it, so the count only
grows -- claims.bounded.tools renders it as "114 finished, still labelled"
and says outright that it only grows until someone drains it. Ten further
claims in `.github-private` are open with no linked PR and no session
behind them, several dating to 08-07; those are the ones with real
exclusion cost, because a claimed issue reads as taken to every session
that looks.

Built as the pr-sweep sibling: weekly cron (a real run, per the #713
standing decision) plus dispatch with dry-run default. Per claimed issue:
exempt a standing claim; release if the issue is closed, whatever closed
it; release if open, PR-less and untouched for stale_days (default 14 --
claims are leases on attention, shorter than the 30-day PR window); keep
everything else with the reason printed. Release is label removal, an
unassign and one comment naming the run -- exactly what every claim record
already tells its reader to do, made mechanical.

THE EXEMPTION IS LOAD-BEARING. The six machine-lane standing claims are
open, PR-less and never refresh, which is precisely the shape the
staleness rule deletes. `.github-private`#731 predicts this sweep by name:
"a standing claim never refreshes, so any wall-clock expiry (#717) would
expire it first." Without the exemption the first cron breaks every lane
in the org, and for the three fan-out lanes in ~86 repos at once.

So two guards, because one was not enough. The exemption keys on a label
rather than on parsing a `machine/*` claimant out of hand-written comment
prose, which is not a checkable interface. And the staleness rule refuses
to run at all in a repo where that label does not exist, saying why: if no
standing claim there can be marked, rule 3 is not safe to apply. The
closed-issue rule is unaffected either way, since a standing claim is open
by construction -- so the 92-issue drain works before the label rollout
and the dangerous half stays off until it is done.

Unassign failure is reported, never fatal: the label is the half pr-claim
reads, so its removal is what ends the claim; the assignee is a
human-facing projection and must not be load-bearing.

The release comment is built with `jq -nc` rather than inline prose --
this repo's workflow ratchet refuses content at column 0 in a run: block,
and caught exactly that defect in the first draft.

Rollout steps that are not in this diff are checkboxes on #286: create the
`standing-claim` label in both repos and apply it to the six standing
claims, and register the `claim-sweep` broker entry. Until the broker
entry exists, cross-repo runs fail at mint naming the gap -- the intended
arrival state; same-repo runs work on the workflow's own token.

Tests: 254 root, 207 .claude, 93 workflow ratchets, all passing.

Closes #286

Co-Authored-By: Claude <noreply@anthropic.com>
Run 2 -- the first real run -- died on its first release with:

    unknown shorthand flag: 'd' in -d

`gh api` has no `-d` flag. That is curl's. `_claim.yml` writes
`-d "$(jq -nc ...)"` and is correct, because `_claim.yml` calls curl;
I read that line as a house idiom and carried it to `gh api`, where it
is simply not a flag. The dry run could not catch it: the release
branch is the one path a dry run never enters, so the defect was
structurally invisible until DRY_RUN=false.

Nothing was mutated. `set -euo pipefail` aborted on the comment POST,
which runs before the label removal, so no claim was half-released and
the count stayed at 23. That ordering was already deliberate -- the
`.github-private`#652 shape is a label removed with no record of why --
and it is what turned a wrong flag into a clean no-op instead of a
partial sweep.

Both body-carrying calls now pipe JSON to `--input -`. The unassign
additionally sends one request for all assignees rather than one per
login, and keeps its `|| warning`: under `set -e` a non-assignable
login after the label came off would abort mid-release, which is the
same #652 half-applied shape. The label is the half `pr-claim` reads,
so once it is gone the claim is released; the assignee is a projection.

Verified before pushing: no `gh api ... -d` remains anywhere in the
file, and 254 root + 207 .claude + 93 workflow ratchets pass.

Reopens the ticket, because the merged lane did not work: the sweep
is not delivered until a real run releases something.

Closes #286

Co-Authored-By: Claude <noreply@anthropic.com>
#287 was squash-merged, so main carries one squashed commit while this
branch still had the pre-squash original plus the fix on top. The
histories diverged and GitHub reported the PR as `dirty` -- an add/add
conflict on claim-sweep.yml, both sides having "created" the file.

Resolved toward this branch: main's copy is the pre-fix version whose
release path uses `gh api -d`, which is not a flag and is exactly what
run 2 died on. Taking main's side would have re-landed the defect.

Verified after resolving: no conflict markers, no `gh api ... -d`
anywhere in the file, four `--input -` call sites, and 254 root +
207 .claude + 93 workflow ratchets all passing.

Co-Authored-By: Claude <noreply@anthropic.com>
)

The lane shipped with a cross-repo fan-out: a `repo` input (or `*`) and a
token minted through the OIDC broker under a `claim-sweep` entry. That
entry does not exist and could not be created from here -- the binding
plane lives in `infra` -- so every cross-repo run would have failed at
mint, and `.github-private`'s 70 claims sat unreachable behind an infra
change in a third repository. The lane worked on exactly one repo: the
one it lives in.

A reusable needs no credential at all. `_claim-sweep.yml` runs against
the CALLER's `github.repository` with the CALLER's `GITHUB_TOKEN`, and
`issues: write` is exactly what releasing a claim uses -- the same
argument `_claim.yml`'s header makes for why that door, alone of the
three, travels. Adopting the sweep becomes a short caller rather than a
broker grant, and no fan-in App grows `issues: write` because one window
needed it.

It is also the shape this org has converged on twice already. `_claim.yml`
records what the alternative cost: the claim convention was delivered as
"copy this file", the copies drifted, and one of them left issues
labelled `claimed` with no comment naming anyone -- unheld, unclaimable,
clearable only by hand (#652). `_pr-claim.yml` is the same pattern. A
third copy of sweep logic was the wrong direction.

Behaviour is unchanged and deliberately so: the four rules in order, the
standing-claim exemption, both fail-closed guards, comment-before-label,
and the one-request unassign with its non-fatal warning all move across
verbatim. The `-d`/`--input` correction from #288 moves with them, with
the reason recorded at the call site so it cannot be reintroduced by
someone reading `_claim.yml`'s correct `curl -d` as a house idiom.

The reusable also emits `released` as an output, so a caller can act on
whether anything actually changed -- which is what a claim-activity
dispatch will need.

Verified: 254 root, 207 .claude, 95 workflow ratchets (up from 93; the
new file picks up its own checks). Evidence the behaviour is right is
run 33264394589 on the pre-refactor file -- `released 23, kept 0,
exempted 5` against .github, with all five standing claims printing
`exempt`, and claims.bounded.tools moving 114 -> 92 once the feed
republished.

Closes #286

Co-Authored-By: Claude <noreply@anthropic.com>
`front-desk-feed`'s publish lane already ACCEPTS
`repository_dispatch: types: [pr-activity, claim-activity]`. The
claim-activity type was added in front-desk-feed#6 for exactly this, and
that header names what is missing: "No sender emits `claim-activity`
yet; the claim doors are the natural one, since they are the only things
that know a claim changed."

This is a claim door, and after a real sweep it is the only thing in the
org that knows a batch of claims just stopped being claims.

WHY IT MATTERS. The feed's `35 * * * *` cron is best-effort and GitHub
drops slots under load -- a denser cron is skipped MORE often, not less.
Measured 2026-08-28: seven stale claims released between ~21:20Z and
~21:35Z, and claims.bounded.tools went on listing all seven as live work
-- one already closed and merged -- from an 18:55:58Z snapshot until a
hand dispatch at 22:38Z. Measured again today: the .github sweep landed
at 16:59Z and the page still read 114 at 17:04Z, moving to 92 only after
this session dispatched the publish lane by hand.

Fires only when something actually changed: never on a dry run, never
when a real run released nothing. Otherwise the weekly no-op cron would
spend a publish run republishing an identical feed.

INERT UNTIL PROVISIONED, the same posture the receiving half shipped
with. repository_dispatch needs `contents: write` on the target, which a
caller's GITHUB_TOKEN does not carry for another repository, so it wants
a broker binding-plane entry bounded by an installation on that repo
alone. Absent `feed_dispatch_token` the step prints a notice and exits 0.
A stale page is not a reason to call a successful release failed, so the
step is also `continue-on-error`.

THE TOKEN IS CHECKED IN THE SHELL, NOT IN THE `if`. The secrets context's
availability in a step conditional is the kind of thing that evaluates to
something surprising rather than erroring, and #289 is the standing
record of shipping a branch no dry run reaches. An empty env var is
unambiguous on every runner.

The payload carries no issue-derived data and no repo name: every event
produces the same whole-board reprojection so there is nothing to branch
on, and a caller here may be private while the receiver's logs are
world-readable. front-desk-feed's header refuses to print its payload for
the same reason.

Verified: 254 root, 207 .claude, 95 workflow ratchets; no `gh api ... -d`
anywhere in the file.

Closes #290

Co-Authored-By: Claude <noreply@anthropic.com>
@bdelanghe
bdelanghe marked this pull request as ready for review August 29, 2026 17:32
@bdelanghe
bdelanghe requested a review from a team as a code owner August 29, 2026 17:32
@bdelanghe
bdelanghe merged commit ce3ba80 into main Aug 29, 2026
10 checks passed
@bdelanghe
bdelanghe deleted the claude/bounded-desk-tools-3fcown branch August 29, 2026 17:33
@bounded-systems-front-desk bounded-systems-front-desk Bot moved this from Todo to Done in Front Desk Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

claim-sweep only works in one repo: the cross-repo fan-out needs a broker entry that does not exist

2 participants