issueflow-reconcile — the sweep flags what the window and collision rules forbid: unblocked twins, and an unblocked non-member during a standing window
`.github/labels.conf` named five identities and every one of them 404s on
this instance — a GitHub-shaped team that was never minted here. Both
consumers of that roster were inoperable in consequence: `panel=` becomes
the required-verdict set, so a review round could never converge, and
`triage-actors=` is the arrival author gate, so every issue was a stray
mint owing `needs-triage` that nobody the conf recognized could clear.
Measured on #191: stamped `needs-triage` four seconds after mint by the
arrival path working correctly, then unclosable for the rest of the day.
The mapping is @andres's ruling, recorded on #191. `cluade` holds triage
and reviews but does not build; `codex` builds; `kimi` reviews; `grok`
comes off the bench; the human row is `andres`.
CONTRIBUTING's roster table moves with the conf, including the approval
count: panel-minus-author resolves to two on this roster, not three,
because the only builder is itself on the panel. The rule is unchanged and
still stated as panel-minus-author — only the number it currently comes to
is named honestly.
test/labels.test.sh now holds the conf and the table to the same set in
both directions. It cannot reach the half that actually broke — two files
agreeing with each other and neither with the forge — but it does catch a
roster edit that touches one file and not the other, which is how a
deliberate swap becomes a silent one.
Not touched, deliberately: drills/*.md, which record runs that really
happened under the old names; REVIEWER.md, whose old-name hits are
citations and a past-event anecdote rather than roster definition.
Refs #195
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D3b corrected the window flag to fire on `claimed` too, PR in flight or
not; the fragment still described it as a `ready` non-member, which is
the reading the ruling struck.
The family-scoping fixture asserted that a foreign marker does not
SILENCE the flag, which a family-blind grep satisfies too. The property
that is load-bearing is the other one: a foreign family late on the
thread must not make the flag re-post. Made family-blind, the sweep now
reds.
D3b says a claimed non-member is flagged whether or not it has an open
PR, and the fixtures covered that for the collision flag only — the
window side, which is the flag the 18:11Z correction was about, had no
case at all.
check() matches its expectation as a substring, so a bare
`issueflow-reconcile` row was satisfied by `issueflow-reconcile.test`
too: the multi-extension rule stayed green under a normalization that
strips only the last extension. keys_of brackets each key, and the
no-em-dash row asserts its emptiness through grep rather than through an
expectation check() cannot make.
Beside them, the cases the two fixes are named for: a self-folding + title
answers its key once and never chains an issue to its own number, and
`needs-triage` and a label-less issue are outside both flags.
B1: deliverable_keys answered a multiset, so a `+` title whose segments
normalize to one key made collision_flags find the issue adjacent to
itself and chain it to its own number — the comment asked an issue to
declare `Blocked by` itself, and two such carriers corrupted the chain
between them. The keys are deduped where the set property belongs.
B2: window_in_scope excluded only blocked/epic/post-merge, admitting
`needs-triage` and label-less issues, so the sweep could add
`needs-triage` to an issue and then tell it about a mint-time membership
call in the same pass. Both flags now call one unblocked_claimable
predicate — #293 D2 corrected gives one gloss on `unblocked` and D3b
says D3 uses it.
The window log line says "unblocked", not "ready": D3b corrected exactly
that wording, and the flag fires on `claimed` too.
Triage ruled all three questions (18:11Z) and corrected the corpus: at the
10:28:54Z mint #257 was `ready`, not `claimed`, and #253 was `claimed`
with NO open PR — #285 was not created until 10:49:16Z. The morning board
takes that shape, which also makes the six `ready` non-members come out as
six, and puts the blocked twin on the board rather than only in a decision
probe.
D2's parenthetical is struck: unblocked = open and not blocked, both flags,
one definition. D3b is corrected in the same direction — a non-member
`claimed` WITH an open PR is flagged too, because a non-member holding a
builder and a review round is #292's competition realized, not a mild case
of it. Nothing in the implementation moves: neither flag ever consulted PR
liveness.
Three cases added, so the fixtures pin the rulings and not the prose:
- both carriers `claimed` with their own PRs open -> still flags. The
ninety-three minutes from #285's creation to its merge are exactly when
the struck parenthetical went silent on a live collision, which made the
flag's firing a property of someone's workflow rather than of the board.
- a fifteen-member declaration with every member closed -> D3 dormant, and
the release issue never flagged as its own non-member. A gate declaration
never empties; the precondition is its OPEN members, read off the board.
- flagged -> resolved -> recreated unchanged -> silent, asserted as D4's
stated boundary rather than left accidental.
Plus today's board — the `blocked` sink, a `claimed` gate member, two
`blocked` issues — which draws nothing. Verified live as well as in
fixtures: a DRY_RUN sweep of this branch against heavy-duty/ceremony's real
board draws zero flags of either kind.
Two advisory flags on the issue-flow sweep, the mechanical backstop for
#288's collision rule and #292's window rule. Both are prose today, and
both failed silently on the same morning: #284 was minted `ready` into a
file another issue held claimed with a PR in flight, and six `ready`
non-members raced an emptying gate. #262 measured the pattern — the same
class of rule, once in a guard, produced zero misses.
Comments only (D1): no label write, no state change, no new label. The
sweep never guesses intent; it states the board fact and triage resolves.
The collision key is the title's em-dash prefix NORMALIZED, because the
2026-08-04 miss spelled one deliverable two ways — `actions/issueflow-reconcile`
against bare `issueflow-reconcile` — so exact-prefix matching would have
missed the pair it was written for. One leading path segment comes off,
then every extension; a `+`-joined title matches on any segment.
The flag asks for a CHAIN, not a fan (#288 D3): within one key each issue
names the newest open carrier below it, so the declaration it asks for
releases exactly one successor per close.
A standing window is a release issue whose gate still holds an OPEN member.
The board read IS the open set, so membership decides openness with no
extra call, and an all-closed gate is the emptied gate the release's own
blocked -> ready promotion answers — which is why a `ready` release leaves
the flag dormant instead of flagging the whole board.
Dedup is the declaration echo's, extracted into state_marker /
state_echo_needed and scoped per family (D4): the marker is keyed to the
offending state's value and compared against that family's last word on
the thread, so a state that changes always speaks.
Fixtures replay the 2026-08-04 morning board whole and the post-ruling
board beside it: the first draws exactly four collision flags and six
window flags and writes not one label; the second draws none.
Closes#293.
Both checks ride the existing sweep walk and write nothing but comments
(#293 D1). The board read now answers the whole payload, because both
decisions are over the WHOLE board — every open issue's labels and title,
and every open release issue's body — and a second pagination for the same
rows would be a second board free to disagree with this one mid-sweep.
Fixtures still owed.
lib/attention.sh had #267 D4's premise exactly — both reconcilers source
it, nothing release-side does — and [scope:release-flow] alone was a
wrong answer of the class that decision exists to correct. The sweep
workflow pair joins beside its trigger pair (detached in #209), the
shared-lib tests take scope:labels alone (a test inherits no lib/**
glob), and D4's seven enumerated rows land one each. No catch-all, by
decision: both directories span all four scopes. Each of the 13 new map
rows is protected by its own assertion — deleted alone, each reds
exactly its case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the mint-time membership call, the sink/source/subset invariants, and the release note while the contradictory successor-count criterion awaits triage clarification.\n\nCloses #292.
The state-machine fixtures bound BOT1/BOT2/BOT3 to REQUIRED_BOTS by index off
the shipped .github/labels.conf, so three fixtures silently required a
four-member panel=. Shrinking it to three left the third slot unbound and
set -u aborted the file before assertion 1: 217 assertions became 0, on main
and on every branch cut from it.
The fixtures now write their own conf, in test/labels.test.sh's shape, at all
three load sites (top of file, the #205 re-drafted-round block, and the
mutant_blockers subshell). One live-file case survives as a property — the
shipped conf parses and recuses each member from its own panel — with no
index and no expected size, and a copy whose panel= names nobody proves it
still has teeth.
Refs #304
Claiming a needs-ruling issue dated it through the assigned timeline
event, silencing the 7-day escalation nudge at exactly the moment a
builder started working through it. The ruling block now reads
last_issue_comment_activity (D1); the reclaim clock keeps the assignment
(D2) because there the assignment IS the claim; post-merge hands its
evidence read to the ruling block instead of reading again (D6, D7); the
claimed branch reads both clocks at its top, before anything it posts.
LABELS.md and lib/ruling.sh now say what each surface's clock reads
(D4, D5). The #257-era order compositions move to the comments read —
the timeline is no longer an input the issue clocks take, and its
unreadability no longer holds unrelated writes hostage; that narrowing
is pinned rather than implied.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The section is re-assembled: main's merge base now carries two fragments
(188.md and 191.md), so 0.4.1 publishes with the door port it actually
ships, and neither fragment survives its own release.
drills/0.4.1.md takes main's two-run record, with run 1's disclosures
restored — the scratch repo it used, its candidate ref, and the ~8 minutes
it spent public to read job logs. A later success does not retire a
disclosed deviation.
The D3 guard loop passed the action path and its test path to derives in
one call. derive_labels emits scope:guards when either matches, so any one
of the six rows could be deleted with the case still green — three
assertions standing in for six rows. Split into one assertion per path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex (!194#1588) is right on the text: #191's criterion is a drill that
runs all six probes with 1 and 5 passing, not two probes passing and four
recorded. Probe 2 (mislabeled ordinary PR) and probe 4 (a re-run of the
completed ceremony) were run on the same consumer, un-archived for them and
archived again after.
Probe 4 diverges in mechanism because Forgejo 8.0.3 has no run-rerun API:
the ceremony was re-run by reproducing its input rather than replaying the
run. The record says so, and says which assert refused.
#191's last acceptance criterion was a live drill against the MERGED tree,
not the candidate. Run against `fda5657`:
probe 1 merge door one release 0.4.1, changelog body, main re-armed
to 0.4.2-dev, both assets uploaded
probe 3 no label refused, nothing created
probe 5 tag door 0.5.0 published, main VERSION untouched
probe 6 bad tag refused, nothing created
The fixture carried an artifact hook this time, dropping `drill asset.tgz`
and `a&b.tgz`. Both survived under those exact names — the encoding fix
proven end to end, in the place it would have failed: after the tag exists,
mid-publish.
The record keeps run 1 (the failure at 9a229ee) beside run 2, because the
failure is why #191 exists and a record that quietly replaced it would be
the kind of tidy history this repo refuses.
Refs #191
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The red-head rule in Picking carried its own trigger definition at the
pre-slim head; the squeeze took it, leaving 'failing check' and 'red
check' classified only forward in the review round. At an all-cancelled
head that let parked shape 2 read satisfied on its face, parking a claim
the rule says is never parked (#163, #276).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>