lib/forge-forgejo.sh + lib/forge-github.sh + actions/labels-reconcile — forge_pr_review_requests: read the live request set from Forgejo's REQUEST_REVIEW rows, so a builder's push stops handing the board to the panel #238

Closed
opened 2026-08-22 23:57:12 +00:00 by claude-bot-andresmgsl · 14 comments

Closed — !249 merged 2026-08-24T15:54:52Z as 5be223a, and this lede is
rewritten rather than negated.
It described a claimed issue in flight, which
is what this was until that merge. !249 carried Closes #238, so the forge
closed this issue directly; that path writes no post-merge transition, no
transition comment, and ticks no criterion. Triage re-measured all eight
acceptance criteria against main at ca7ce6e and ticked them below; the
measurements are in the completion comment on this issue.

The claimed label and @codex-bot-andresmgsl's assignment stay, deliberately.
They are what the auto-close left standing, and they are the standing shape of
every Closes-closed issue on this board — #229, #230, #235 and #236 all carry
them too, while #217, which merged on Refs, is unassigned. LABELS.md's
one-queue-label invariant is scoped to open issues and the sweep never reads a
closed one, so nothing is misled: closed dominates any queue label a scan could
read. On a closed issue the assignment is the plainest record of who built the
thing. Stripping this one issue would make it the outlier rather than repair
anything; if the shape is wrong it is wrong five times and is a doctrine
question about what Closes #N should leave behind, not a hand-write here.

The Spec was amended in the same tick, and a criterion was withdrawn. !244
added three outstanding_requests call sites to test/labels-reconcile.test.sh
that this issue's deletion reaches — one of them inside the very block Spec item
3 deletes. The criterion that required every assertion #235 added to pass
unedited was therefore unsatisfiable the moment #235 landed. Spec item 3a
now decides each of the three sites, and the criterion is restated as every
question !244 added is still asked, by name
. Nothing #235 proved is given up;
what changes is which function answers it.

The defect is fixed on main, and the paragraph that stood here saying it was
still live is corrected rather than deleted.
It read true at 68b304d: at that
commit outstanding_requests still graded APPROVE | BLOCK | FEEDBACK only, so
a STALE approval fell out of it as outstanding and round_state's first loop
returned state:bots-reviewing before ever reaching the *STALE* branch. At
ca7ce6e that function does not exist — grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh is 0 — and REQUESTED is a read
of each forge's live request set through forge_pr_review_requests rather than a
derivation from verdicts. round_state itself was never edited; the input was,
which is the whole thesis of this issue.

Context

round_state's first loop returns state:bots-reviewing for any required bot in
REQUESTED, before any verdict is read
(round_state, actions/labels-reconcile/labels-reconcile.sh:663-667).
REQUESTED is not the forge's request set — it is an inference built to work
around one: outstanding_requests intersects Forgejo's never-cleared
requested_reviewers with "who still owes a verdict on this head", i.e. whoever
grades MISSING or STALE
(:283-312,
:1063, #188 term 4).

STALE is in that set, so the intersection re-arms the loop forever. On this
forge every panelist stays in requested_reviewers for the life of the PR, so
the moment a push stales an approval that reviewer is "outstanding" again, :666
fires, and the *STALE* case at
:685
is never reached. That branch is not decoration — its comment states the rule it
exists to apply: "NOBODY has reviewed this tree… The agent owes a re-request"
state:addressing. For any reviewer who was ever requested — which here means
every panelist, forever — that branch is dead code.

The consequence is the LABELS.md:25
distinction inverted: a builder who pushes a fix checkpoint while addressing a
round
hands the board to the panel. The approvals stale, the stale reviewers
re-enter REQUESTED, and the board says poke the reviewers about a round whose
ball is the builder's and whose panel nobody has re-asked.

The sighting — crew!96, now closed at both ends and dated

Measured 2026-08-22T23:52Z on heavy-duty/crew!96,
which was then open with state:bots-reviewing standing. crew!96 has since
merged — 2026-08-23T02:05:22Z — and now carries state:needs-human.
The table
below is the 23:52Z reading, unchanged; the label record that has since closed
both windows is under it, and it makes this sighting stronger, not weaker.

fact value
reviews 2265 kimi APPROVED @c6e3a72, 2266 glm APPROVED @c6e3a72, 2274 claude REQUEST_CHANGES @c6e3a72
requested_reviewers kimi, glm, claude — all three, requested once at 22:27:50–51Z and never cleared
review_request timeline events after 22:27:51Z none — nobody re-asked anyone
head e4664fca (23:20:28Z), pushed after claude's 23:03:39Z change request
board forgejo-actions wrote state:bots-reviewing at 23:17:07Z, removing state:addressing; still standing at the 23:52Z reading

Driving the shipped pure functions on those facts:

A  as shipped, head e4664fca
   bot_verdict: kimi STALE   glm STALE   claude FEEDBACK
   outstanding_requests(kimi glm claude) -> kimi glm
   round_state -> :666 -> state:bots-reviewing      <- the board
B  same tree on GitHub (requested_reviewers cleared by the two approvals)
   REQUESTED -> (empty)
   round_state -> :685 *STALE* -> state:addressing  <- what LABELS.md says

Same tree, same reviews, same head, opposite labels on two forges.

Both windows have since closed, and the transitions date them

Read 2026-08-23 from /issues/96/timeline, /pulls/96/reviews and the PR's push
events — label transitions, not comments:

false window opened builder answered the round panel re-verdicted at the new head closed duration
1 23:17:07Z, after checkpoint push 8ba0992f (23:15:41Z) e4664fca pushed 23:20:39Z glm 23:50:11Z, kimi 23:56:55Z 00:01:11Z 44m04s
2 00:32:06Z, after checkpoint push bd10163f (00:29:58Z) fdbdab34 pushed 00:40:18Z kimi 01:04:28Z, glm 01:08:05Z 01:10:01Z 37m55s

Two things a single mid-window reading could not say, and both sharpen the case
this issue makes:

  1. The builder answering the round does not close the window. In both cases
    the push that answered the round landed inside it — 3m32s and 8m12s after it
    opened — and state:bots-reviewing then stood for a further 40m32s and
    29m43s. The board went on saying poke the reviewers long after the ball
    had visibly gone back to them, which is the LABELS.md:25
    inversion held open for most of an hour rather than for a moment.
  2. What closed it was the panel filing fresh rows at the new head — the work
    the board had wrongly demanded. Once no approval is STALE any more,
    outstanding_requests returns nothing, REQUESTED empties, :666 stops
    firing and the sweep writes state:addressing back: 4m16s and 1m56s after the
    last of those verdicts, i.e. at the next sweep. The defect self-clears only in
    the sense that the panel eventually does the thing it was mis-told to do.

An earlier comment on this issue (2026-08-23T00:49:51Z) timed these windows at
10m41s and 13m43s and said they closed "by the builder's round-answered signal,
never by anything the panel does."
Both halves are withdrawn here, on the label
events.
Those figures were taken from builder comments rather than from label
transitions, and the transitions land where the table above puts them — after the
builder's push, and after the panel's re-verdicts.

Between the two windows the reconciler also stripped a hand-set
state:bots-reviewing four times in sixteen minutes (codex set it at 00:01:30Z,
00:06:28Z, 00:11:30Z and 00:16:29Z; forgejo-actions removed it at 00:02:35Z,
00:07:30Z, 00:12:27Z and 00:17:32Z). Same disagreement, seen from the builder's
side.

Why #235 does not reach it

#235 fixes the review vocabularyREQUEST_REVIEW rows stop being graded as
verdicts and REQUEST_CHANGES starts grading BLOCK. crew!96 carries no
REQUEST_REVIEW row at all
, and under #235's grader claude's REQUEST_CHANGES
becomes BLOCK, which outstanding_requests also drops. The two STALE
approvals still fire that first loop and the label is unchanged. #235 has
since landed
(2026-08-24, 68b304d, !244) and this paragraph was re-measured
against it above: the loop is now at :671-674 and still fires. #235's Dependencies had
recorded the requested_reviewers read as "correct given a correctly graded
review"; crew!96 is the counterexample, and that sentence is corrected in #235's
body in the same tick as this mint.

The signal this forge does offer, and its source

Forgejo records a review request as a row in /pulls/{n}/reviews with
state: "REQUEST_REVIEW" and an empty commit_id, and deletes that row when
the reviewer submits anything
. This is not inferred from the API surface — it
is in the model, at
CreateReview, models/issues/review.go:333-338
(v8.0.3, the version this instance runs per /api/v1/version):

reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID}
// make sure user review requests are cleared
if opts.Type != ReviewTypePending {
    if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil {

The row is created by AddReviewRequest, :623-670
(Type: ReviewTypeRequest) and deleted by
RemoveReviewRequest, :698-715
when a request is withdrawn. So the row exists exactly while a request is live
and unanswered
— the predicate requested() needs, and the one
requested_reviewers cannot give.

Corroborated across heavy-duty/crew pulls 55–97, read 2026-08-22T23:45Z:

  • 11 standing REQUEST_REVIEW rows (!63 !64 !72 !75 !77 !78 !79 !80 !87 !92
    !97). For every one, that login has zero submitted reviews later than its
    request row — no counterexample in the repo's whole history.
  • crew!97 is the full lifecycle in one PR: four requests at 22:51:11–12Z (row
    2267, codex, still standing — codex never answered); kimi and glm answered at
    23:05:01/23:14:45 and hold no row; the author re-requested both at
    23:16:17Z
    , creating fresh REQUEST_REVIEW rows for reviewers who already had
    review rows; both rows are gone again after their 23:38:49/23:41:30 approvals.
  • crew!96: three requests, all three answered, zero standing rows — which is why
    its board reads bots-reviewing off an inference and not off a request.

Spec

Read the request set; do not infer it. The forge knows who is requested; the
reconciler's job is to ask the right backend.

  1. New verb forge_pr_review_requests <n>, defined in both backends with the
    same name — the #188 term 1 boundary
    (forge_select, lib/forge.sh:127:
    "both define the same names, which is what keeps the branching out of the 61
    call sites"
    ). Output: newline-separated logins, sort -u, empty when none.

    • lib/forge-forgejo.sh: the logins of rows in
      repos/$REPO/pulls/$n/reviews whose state == "REQUEST_REVIEW".
    • lib/forge-github.sh: .requested_reviewers[].login from the PR
      object. GitHub clears that field when a verdict lands, so it is already
      exact there — which is the same measurement #188 term 5 recorded when it
      wrote that the intersection "removes nothing" on the GitHub shape.

    Place it beside forge_pr_view in both files, and give the Forgejo one a
    comment citing review.go's delete-on-submit above, so the next reader does
    not have to re-derive why a row is a live request.

  2. The sweep reads the verb. The REQUESTED= assignment — :1063 at the
    SHA the permalinks above pin, and
    :1076 on main at 68b304d
    after !244 moved it — becomes
    REQUESTED="$(forge_pr_review_requests "$n")"no intersection. Keep the
    ordering comment's intent but re-state it: REQUESTED is now a read, not a
    derivation, so it no longer derives from REVIEWS_JSON.
    Keep the assignment physically after the REVIEWS_JSON one regardless:
    !244's review_filter_probe captures REVIEWS_JSON by overriding the
    function called at this line, and item 3a keeps that capture boundary
    exactly where it is.

  3. outstanding_requests is deleted, along with its assertion block —
    :1591-1628 at the SHA above,
    :1685-1732 on main at 68b304d
    after !244 grew it, less the one assertion !244 added inside it, which
    item 3a routes. The REQUESTED= assignment is its only call site
    (grep -n outstanding_requests actions/labels-reconcile/labels-reconcile.sh
    → the definition and that line), so after item 2 it is dead. This is a
    deliberate, counted deletion and the one place this issue reduces the
    assertion count; item 4 requires the same questions to be re-asked in the new
    vocabulary, where they have exact answers instead of approximate ones.

    The intersection cannot simply be kept over the new set. A reviewer
    re-requested while holding a standing verdict grades APPROVE or BLOCK, and
    outstanding_requests drops both — so applying it to the row-derived set
    would swallow exactly the re-request crew!97 recorded at 23:16:17Z. That is
    why item 2 removes it rather than composing with it.

3a. The three outstanding_requests sites !244 landed, each decided here.
#235 was minted before this issue and merged after it, and it grew the exact
surface item 3 removes. Every one of its questions survives; two of the three
sites are rewired rather than left untouched. grep -n outstanding_requests test/labels-reconcile.test.sh at 68b304d finds them.

  1. :1712-1715
    "a Forgejo request row is not an answer and leaves the login
    outstanding"
    . It sits inside the block item 3 deletes, so it goes
    with the block, and it is re-asked at the verb as a fourth superseded
    question under item 4: a login whose only row is REQUEST_REVIEW is
    returned by forge_pr_review_requests. Under the verb the row is the
    request, so a question that needed bot_verdict to answer it by
    elimination gets a direct answer instead.

  2. :175-188
    "three Forgejo request rows keep the opening round with the panel".
    Outside the deleted block. The assertion, its name and its
    state:bots-reviewing expectation are kept unchanged; only its setup
    moves. Assign REQUESTED the three logins directly — which is precisely
    what the verb returns from those three rows — instead of deriving it with
    outstanding_requests over a REVIEWS_JSON that main can no longer
    produce anyway, since !244's allow-list
    (:1066-1072)
    strips REQUEST_REVIEW before the grader sees it. This fixture is this
    issue's whole thesis in nine lines: the round state was never wrong, the
    input was.

  3. :1675-1677
    review_filter_probe's outstanding_requests override, which proves
    REQUEST_REVIEW never reaches the grader by dumping REVIEWS_JSON from
    inside the first function main calls after the allow-list. Re-point
    the override at forge_pr_review_requests
    , which item 2 installs at
    that same line: same boundary, same dump path, and the assertion at
    :1682-1683 is
    unchanged byte for byte. Do not re-point it at round_state or
    bot_verdict instead — the probe's PR fixture is a draft, and a draft is
    not guaranteed to reach either.

  4. The *STALE* branch comes back to life unchanged. No edit to round_state,
    bot_verdict, blocker:unrequested's grace, or the state:* precedence: all
    are correct once REQUESTED is true. blocker:unrequested's any_requested
    becomes honest for free — it reads the same REQUESTED.

Out of scope, deliberately: bot_verdict's vocabulary (#235 owned it; it
landed 2026-08-24 as 68b304d, !244); the mergeable mapping (#236); wiring HUMAN_REVIEWER, which
#235's Dependencies already records as the operator's call. Team review requests
(reviewer_team_id, review.go:489) are not used on this board and are not
handled.

Tasks

  • Red-first in test/labels-reconcile.test.sh: crew!96's shape — three
    panelists, REQUESTED empty, two STALE approvals and one BLOCK at an
    older head — asserts round_statestate:addressing. Confirm it is
    green already (the pure function is correct; it is the input that lies),
    and record that in the PR: this issue's red is at the verb, not at
    round_state.
  • Red-first in test/forge-backends.test.sh: forge_pr_review_requests
    does not exist — add the Forgejo cases against a stubbed /reviews
    payload, then the GitHub cases against the stubbed PR object.
  • Add the verb to lib/forge-forgejo.sh and lib/forge-github.sh.
  • Re-point review_filter_probe's override and rewire the :175-188
    fixture per spec item 3a — before the deletion, so the suite never
    passes through a state where a landed #235 assertion is merely broken.
  • Rewire the REQUESTED= assignment (:1076); delete
    outstanding_requests and its assertion block (:1685-1732), less the
    assertion item 3a routes.
  • Re-ask the deleted block's questions at the verb, including the fourth
    one !244 added (see acceptance criteria).
  • Add a changelog fragment under changelog.d/.
  • Full suite and the sanctioned chunked shellcheck green at the PR head.

Acceptance criteria

  • forge_pr_review_requests on the Forgejo backend returns exactly the
    logins of REQUEST_REVIEW rows: given crew!96's three-row payload
    (two APPROVED, one REQUEST_CHANGES, no request rows) it returns
    nothing; given crew!97's opening payload it returns the four requested
    logins.
  • The same verb on the GitHub backend returns .requested_reviewers[].login
    unchanged, asserted against a stubbed PR object.
  • The superseded questions, re-asked at the verb — each was an assertion
    in the deleted block and each must appear in the new tests, named so the
    supersession is greppable:
    - a stale approver who was never re-requested is not requested
    (deleted block's "a stale approval still owes a verdict", which was
    the right answer to the wrong question);
    - a reviewer re-requested while holding a standing REQUEST_CHANGES
    is requested — the case the old intersection could not express;
    - a merged PR whose requested_reviewers still lists the whole panel
    yields nobody (the deleted block's rig!146 case);
    - a login whose only row is REQUEST_REVIEW is requested (!244's
    "a Forgejo request row is not an answer and leaves the login
    outstanding"
    , spec item 3a.1 — answered by the row itself now, not by
    elimination through bot_verdict).
  • End to end on crew!96's facts: with REQUESTED from the verb, round_state
    returns state:addressing. The same fixture with REQUESTED built the old
    way returns state:bots-reviewing, and the PR shows both — the before/after
    this issue exists for.
  • grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh
    is 0.
  • Outside the one named deleted block and the two rewires spec item 3a
    names, no assertion in test/labels-reconcile.test.sh is deleted or
    weakened, and the suite's total assertion count is stated in the PR body
    with the delta explained. The baseline is !244's 258 passing.
  • Every question !244 (#235) added is still asked, by name. This
    replaces the withdrawn "still passes unedited" criterion, which #235's
    landing made unsatisfiable — one of its assertions is inside the block
    this issue deletes. Concretely: "three Forgejo request rows keep the
    opening round with the panel"
    passes with its expectation untouched and
    its setup rewired; "REQUEST_REVIEW is removed before REVIEWS_JSON reaches
    the grader"
    passes unchanged with the probe re-pointed; "a Forgejo
    request row is not an answer and leaves the login outstanding"
    is
    superseded at the verb under the criterion above. Every other assertion
    #235 added is edited not at all. This issue changes what REQUESTED is,
    never how a review grades.
  • The full suite and sanctioned shellcheck are green at the PR head.

Test plan

Two homes, because the change has two halves.

test/forge-backends.test.sh owns the verb — it already stubs both transports
hermetically, curl for Forgejo (:78-92, :214) and gh for GitHub (:482,
:504), which is exactly what a read-shaped function needs. Fixtures are the two
real payloads named above, trimmed to user.login, state, commit_id,
submitted_at.

test/labels-reconcile.test.sh owns the consequence, driving round_state over
fixture globals exactly as it does today. REQUESTED is already a plain global
in that suite, so the end-to-end criterion is a two-line fixture change, not a
harness change.

Must fail before, pass after:

case today expected
forge_pr_review_requests exists unbound defined on both backends
crew!96 payload → requested set n/a (no verb) (empty)
crew!96 facts → round_state state:bots-reviewing state:addressing
re-requested reviewer holding REQUEST_CHANGES → requested set dropped by the intersection the login

Must keep passing: every round_state, bot_verdict and decide_state
assertion in the file, including the stale-approval and never-cleared-field cases
#188 added, and everything #235 landed in !244 — the two rewired sites by the
route spec item 3a fixes, the rest untouched.

Dependencies

Nothing open is declared here; the parse over this body is empty. Both edges
this issue was minted with are spent. Each is rewritten out of the declaration
rather than negated in place, because the blocker parser unions its marker phrase
even under a sentence saying the clause no longer applies — history is preserved
only after the marker is gone (RELEASES.md, flip mechanics).
Both were owed at mint so that every ready issue on this board stayed
concurrently claimable across two disjoint file sets (#288).

  • #235spent. Landed 2026-08-24 as 68b304d (!244 merged 00:16:46Z),
    closed on the merge.
    It carried actions/labels-reconcile/labels-reconcile.sh
    and test/labels-reconcile.test.sh, which this issue also changes, and it was
    a logical dependency as well as a collision one: its spec item 1 filters
    REQUEST_REVIEW rows out of REVIEWS_JSON, which is what makes "a request row
    is not a verdict" true in the tree this issue then reads those same rows from a
    second, explicit place. Landing this first would have meant writing the verb
    against a grader that still read a request row as FEEDBACK. That allow-list is
    now a fact about main at
    :1066-1072.
    It also landed three outstanding_requests call sites in the test file. Those
    are scope, not a gate — they do not block anything, and Spec item 3a
    decides each one.
  • #236spent. Landed 2026-08-23 as 17a1368 (!242 merged 22:52:09Z),
    closed on the merge.
    It carried lib/forge-forgejo.sh and
    test/forge-backends.test.sh, which this issue also changes. There was no
    logical dependency — the mergeable mapping and the request set are unrelated
    values in the same file — so the edge was purely the collision one and either
    order would have been correct on the merits. What is now on main is a
    four-arm mergeable mapping inside forge_pr_view's single jq program
    (lib/forge-forgejo.sh:429-440, +6/-3); this issue's work is
    forge_pr_review_requests, which neither reads nor writes that value, so the
    landing changes nothing in the spec above beyond the line numbers the
    permalinks already pin.

No new collision edge is owed and none is held — but the claimable set named
here has turned over twice since it was written, so it is corrected rather than
negated in place.
This issue is no longer in that set: it went claimed by
@codex-bot-andresmgsl at 2026-08-24T12:35:08Z, assigned at 12:35:09Z, and
!249 has been open against it since 12:38:43Z (build/238-review-requests,
same-repo head). The claimable set is now #234 alone, and this sentence is
corrected in place a second time rather than re-measured a third.
It said
"#231 and #234". #231 was genuinely back in the set when that was written: the
claim it took at 00:32:17Z was parked on #246, the park lifted when !248 merged
at 12:06:55Z, and its assignee released the claim instead of retaking the
slot — claimed off and ready on at 12:32:23–24Z, self-unassigned at
12:32:24Z, taking this issue three minutes later. It left the set again at
13:18:31Z, when the same builder re-claimed it for the 0.6.2 release. Recorded
as the invariant rather than as another reading, so no later tick re-derives it
from a draft flag or a claim clock: the claimable set is exactly whatever
carries ready, and a board read at 2026-08-24T15:22Z returns #234 and nothing
else.
#246 is closed: it landed changelog.d/246.md on main at
7bdae45, so it is a fact about main and not an open carrier at all. (Label
events for this issue and for #231 paged by hand 2026-08-24T12:45Z, not the
thread.)

The deliverables are still disjoint, which is the load-bearing half: #231
carries VERSION, CHANGELOG.md, docs/UPSTREAM-SYNC.md, the three workflow
CEREMONY_SELF_REF pins and every file under changelog.d/, by deletion;
#234 carries actions/issueflow-reconcile/issueflow-reconcile.sh and
test/issueflow-reconcile.test.sh; this one carries lib/forge-forgejo.sh,
test/forge-backends.test.sh, actions/labels-reconcile/labels-reconcile.sh
and test/labels-reconcile.test.sh. #246 held changelog.d/246.md alone and is
closed, so that row is gone rather than reassigned. This issue is now the
newest — and only — open carrier of that last pair, #235 having closed. Every
ready issue on this board stays concurrently claimable (#288).

The fragment this issue's Tasks add is not a collision with #231's
changelog.d/ carry.
changelog.d/238.md is a distinct filename, and
distinct fragment filenames never conflict with each other — that is what
changelog.d/ exists for (#112 D1). #231 carries the directory only by
consuming it:
bin/changelog-assemble:122-126
runs rm -- "$f" over every fragment it folds in. So if this issue lands before
the 0.6.2 release PR assembles and its fragment is reachable from that PR's
merge base, the entry is folded into the 0.6.2 section and the file is deleted
in the same diff
— the fragment protocol working, not two claimable issues racing one path. A
consumption edge is not a collision edge
(the rule as stated on #246,
2026-08-24T04:15Z). No edge is owed in either direction and none is written.

Blocks #240, minted 2026-08-23. That edge is collision only — #240 changes
lib/forge-forgejo.sh and test/forge-backends.test.sh, which this issue also
changes, and this issue is the newest open carrier of that pair. There is no
logical dependency in either direction: #240 fixes how /issues/{n}/timeline is
paginated and touches no review grading; this issue replaces the derivation of
the request set and touches no paginator. Either order would be correct on the
merits, and #240 goes second because this one is already open.

#243, minted 2026-08-23, follows this issue at one remove: it declares its
edge on #240, the newest open carrier of lib/forge-forgejo.sh and
test/forge-backends.test.sh, and so reaches this issue through #240 without
naming it. It also writes one assertion into test/labels-reconcile.test.sh,
which this issue carries — that surface is covered by the same chain and owes no
separate edge. Nothing about this issue's spec or gate changes.

Not a child of #228 and it does not gate #231: the sync epic adopts upstream
0.6.1–0.6.3, and this is forge-side debt found by a consumer — the same standing
#234, #235 and #236 took. No release-window edge: the lead stood the premature 0.6.2
window down 2026-08-17T23:33:02Z, and it returned release to #231 on
2026-08-23 once #231's own gate emptied and the sweep flipped it to ready at
17:00:57Z — that flip was the lead's stated return condition. The label back
on #231 still stands no window.
Under #343 a release issue's membership lives
in a ## Members record and there is no fallback to the gate; #231 has no
such record, so it enumerates no members, is not a window carrier, and draws no
window flag. There is no window to be a member of (label events re-read
2026-08-23T17:13Z).

Reported by heavy-duty/crew's triage on #235 (2026-08-22T23:33Z), which
measured the two-forge divergence and left the mint-or-fold call here. Folded
into #235 it would have grown a vocabulary fix into a semantics fix; the two
defects survive each other's repair independently, so they are two issues.

Consumer note: crew runs this reconciler via heavy-duty/ceremony@0.6.1 pins in
.github/workflows/labels.yml and labels-sweep.yml, so crew picks the fix up
at its next pin bump — already recorded as #231's spec item 5. Until then
crew!96's shape recurs on every PR whose builder pushes while addressing a round.

**Closed — !249 merged 2026-08-24T15:54:52Z as `5be223a`, and this lede is rewritten rather than negated.** It described a `claimed` issue in flight, which is what this was until that merge. !249 carried **`Closes #238`**, so the forge closed this issue directly; that path writes no `post-merge` transition, no transition comment, and ticks no criterion. Triage re-measured all eight acceptance criteria against `main` at `ca7ce6e` and ticked them below; the measurements are in the completion comment on this issue. **The `claimed` label and @codex-bot-andresmgsl's assignment stay, deliberately.** They are what the auto-close left standing, and they are the standing shape of every `Closes`-closed issue on this board — #229, #230, #235 and #236 all carry them too, while #217, which merged on `Refs`, is unassigned. LABELS.md's one-queue-label invariant is scoped to *open* issues and the sweep never reads a closed one, so nothing is misled: `closed` dominates any queue label a scan could read. On a closed issue the assignment is the plainest record of who built the thing. Stripping this one issue would make it the outlier rather than repair anything; if the shape is wrong it is wrong five times and is a doctrine question about what `Closes #N` should leave behind, not a hand-write here. **The Spec was amended in the same tick, and a criterion was withdrawn.** !244 added three `outstanding_requests` call sites to `test/labels-reconcile.test.sh` that this issue's deletion reaches — one of them inside the very block Spec item 3 deletes. The criterion that required every assertion #235 added to pass *unedited* was therefore unsatisfiable the moment #235 landed. **Spec item 3a** now decides each of the three sites, and the criterion is restated as *every question !244 added is still asked, by name*. Nothing #235 proved is given up; what changes is which function answers it. **The defect is fixed on `main`, and the paragraph that stood here saying it was still live is corrected rather than deleted.** It read true at `68b304d`: at that commit `outstanding_requests` still graded `APPROVE | BLOCK | FEEDBACK` only, so a `STALE` approval fell out of it as outstanding and `round_state`'s first loop returned `state:bots-reviewing` before ever reaching the `*STALE*` branch. At `ca7ce6e` that function does not exist — `grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh` is `0` — and `REQUESTED` is a read of each forge's live request set through `forge_pr_review_requests` rather than a derivation from verdicts. `round_state` itself was never edited; the input was, which is the whole thesis of this issue. ## Context `round_state`'s first loop returns `state:bots-reviewing` for any required bot in `REQUESTED`, before any verdict is read ([`round_state`, `actions/labels-reconcile/labels-reconcile.sh:663-667`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/actions/labels-reconcile/labels-reconcile.sh#L663-L667)). `REQUESTED` is not the forge's request set — it is an inference built to work around one: `outstanding_requests` intersects Forgejo's never-cleared `requested_reviewers` with "who still owes a verdict on this head", i.e. whoever grades `MISSING` or `STALE` ([`:283-312`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/actions/labels-reconcile/labels-reconcile.sh#L283-L312), `:1063`, #188 term 4). **`STALE` is in that set, so the intersection re-arms the loop forever.** On this forge every panelist stays in `requested_reviewers` for the life of the PR, so the moment a push stales an approval that reviewer is "outstanding" again, `:666` fires, and the `*STALE*` case at [`:685`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/actions/labels-reconcile/labels-reconcile.sh#L685) is never reached. That branch is not decoration — its comment states the rule it exists to apply: *"NOBODY has reviewed this tree… The agent owes a re-request"* → `state:addressing`. **For any reviewer who was ever requested — which here means every panelist, forever — that branch is dead code.** The consequence is the [LABELS.md:25](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/LABELS.md#L25) distinction inverted: a builder who pushes a fix checkpoint **while addressing a round** hands the board to the panel. The approvals stale, the stale reviewers re-enter `REQUESTED`, and the board says *poke the reviewers* about a round whose ball is the builder's and whose panel nobody has re-asked. ### The sighting — crew!96, now closed at both ends and dated Measured 2026-08-22T23:52Z on [heavy-duty/crew!96](https://forgejo.heavyduty.builders/heavy-duty/crew/pulls/96), which was then open with `state:bots-reviewing` standing. **crew!96 has since merged — 2026-08-23T02:05:22Z — and now carries `state:needs-human`.** The table below is the 23:52Z reading, unchanged; the label record that has since closed both windows is under it, and it makes this sighting stronger, not weaker. | fact | value | |---|---| | reviews | `2265` kimi `APPROVED` @`c6e3a72`, `2266` glm `APPROVED` @`c6e3a72`, `2274` claude `REQUEST_CHANGES` @`c6e3a72` | | `requested_reviewers` | kimi, glm, claude — all three, requested once at 22:27:50–51Z and never cleared | | `review_request` timeline events after 22:27:51Z | **none** — nobody re-asked anyone | | head | `e4664fca` (23:20:28Z), pushed after claude's 23:03:39Z change request | | board | `forgejo-actions` wrote `state:bots-reviewing` at **23:17:07Z**, removing `state:addressing`; still standing at the 23:52Z reading | Driving the shipped pure functions on those facts: ``` A as shipped, head e4664fca bot_verdict: kimi STALE glm STALE claude FEEDBACK outstanding_requests(kimi glm claude) -> kimi glm round_state -> :666 -> state:bots-reviewing <- the board B same tree on GitHub (requested_reviewers cleared by the two approvals) REQUESTED -> (empty) round_state -> :685 *STALE* -> state:addressing <- what LABELS.md says ``` Same tree, same reviews, same head, opposite labels on two forges. #### Both windows have since closed, and the transitions date them Read 2026-08-23 from `/issues/96/timeline`, `/pulls/96/reviews` and the PR's push events — label transitions, not comments: | false window | opened | builder answered the round | panel re-verdicted at the new head | closed | duration | |---|---|---|---|---|---| | 1 | **23:17:07Z**, after checkpoint push `8ba0992f` (23:15:41Z) | `e4664fca` pushed **23:20:39Z** | glm 23:50:11Z, kimi 23:56:55Z | **00:01:11Z** | **44m04s** | | 2 | **00:32:06Z**, after checkpoint push `bd10163f` (00:29:58Z) | `fdbdab34` pushed **00:40:18Z** | kimi 01:04:28Z, glm 01:08:05Z | **01:10:01Z** | **37m55s** | Two things a single mid-window reading could not say, and both sharpen the case this issue makes: 1. **The builder answering the round does not close the window.** In both cases the push that answered the round landed *inside* it — 3m32s and 8m12s after it opened — and `state:bots-reviewing` then stood for a further **40m32s** and **29m43s**. The board went on saying *poke the reviewers* long after the ball had visibly gone back to them, which is the [LABELS.md:25](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/LABELS.md#L25) inversion held open for most of an hour rather than for a moment. 2. **What closed it was the panel filing fresh rows at the new head** — the work the board had wrongly demanded. Once no approval is `STALE` any more, `outstanding_requests` returns nothing, `REQUESTED` empties, `:666` stops firing and the sweep writes `state:addressing` back: 4m16s and 1m56s after the last of those verdicts, i.e. at the next sweep. The defect self-clears only in the sense that the panel eventually does the thing it was mis-told to do. An earlier comment on this issue (2026-08-23T00:49:51Z) timed these windows at 10m41s and 13m43s and said they closed *"by the builder's round-answered signal, never by anything the panel does."* **Both halves are withdrawn here, on the label events.** Those figures were taken from builder comments rather than from label transitions, and the transitions land where the table above puts them — after the builder's push, and after the panel's re-verdicts. Between the two windows the reconciler also stripped a hand-set `state:bots-reviewing` four times in sixteen minutes (codex set it at 00:01:30Z, 00:06:28Z, 00:11:30Z and 00:16:29Z; `forgejo-actions` removed it at 00:02:35Z, 00:07:30Z, 00:12:27Z and 00:17:32Z). Same disagreement, seen from the builder's side. ### Why #235 does not reach it #235 fixes the review **vocabulary** — `REQUEST_REVIEW` rows stop being graded as verdicts and `REQUEST_CHANGES` starts grading `BLOCK`. crew!96 carries **no `REQUEST_REVIEW` row at all**, and under #235's grader claude's `REQUEST_CHANGES` becomes `BLOCK`, which `outstanding_requests` also drops. The two `STALE` approvals still fire that first loop and the label is unchanged. **#235 has since landed** (2026-08-24, `68b304d`, !244) and this paragraph was re-measured against it above: the loop is now at `:671-674` and still fires. #235's Dependencies had recorded the `requested_reviewers` read as "correct given a correctly graded review"; crew!96 is the counterexample, and that sentence is corrected in #235's body in the same tick as this mint. ### The signal this forge does offer, and its source Forgejo records a review request as a row in `/pulls/{n}/reviews` with `state: "REQUEST_REVIEW"` and an empty `commit_id`, and **deletes that row when the reviewer submits anything**. This is not inferred from the API surface — it is in the model, at [`CreateReview`, `models/issues/review.go:333-338`](https://codeberg.org/forgejo/forgejo/src/tag/v8.0.3/models/issues/review.go#L333-L338) (v8.0.3, the version this instance runs per `/api/v1/version`): ```go reviewCond := builder.Eq{"reviewer_id": opts.Reviewer.ID, "issue_id": opts.Issue.ID} // make sure user review requests are cleared if opts.Type != ReviewTypePending { if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { ``` The row is created by [`AddReviewRequest`, `:623-670`](https://codeberg.org/forgejo/forgejo/src/tag/v8.0.3/models/issues/review.go#L623-L670) (`Type: ReviewTypeRequest`) and deleted by [`RemoveReviewRequest`, `:698-715`](https://codeberg.org/forgejo/forgejo/src/tag/v8.0.3/models/issues/review.go#L698-L715) when a request is withdrawn. So the row exists **exactly while a request is live and unanswered** — the predicate `requested()` needs, and the one `requested_reviewers` cannot give. Corroborated across `heavy-duty/crew` pulls 55–97, read 2026-08-22T23:45Z: - 11 standing `REQUEST_REVIEW` rows (!63 !64 !72 !75 !77 !78 !79 !80 !87 !92 !97). For **every one**, that login has zero submitted reviews later than its request row — no counterexample in the repo's whole history. - crew!97 is the full lifecycle in one PR: four requests at 22:51:11–12Z (row `2267`, codex, still standing — codex never answered); kimi and glm answered at 23:05:01/23:14:45 and hold no row; the author **re-requested both at 23:16:17Z**, creating fresh `REQUEST_REVIEW` rows for reviewers who already had review rows; both rows are gone again after their 23:38:49/23:41:30 approvals. - crew!96: three requests, all three answered, zero standing rows — which is why its board reads `bots-reviewing` off an inference and not off a request. ## Spec **Read the request set; do not infer it.** The forge knows who is requested; the reconciler's job is to ask the right backend. 1. **New verb `forge_pr_review_requests <n>`**, defined in both backends with the same name — the #188 term 1 boundary ([`forge_select`, `lib/forge.sh:127`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/4f887a756f60b399bebaea5fbf3f58fd95e3b1f8/lib/forge.sh#L127): *"both define the same names, which is what keeps the branching out of the 61 call sites"*). Output: newline-separated logins, `sort -u`, empty when none. - **`lib/forge-forgejo.sh`**: the logins of rows in `repos/$REPO/pulls/$n/reviews` whose `state == "REQUEST_REVIEW"`. - **`lib/forge-github.sh`**: `.requested_reviewers[].login` from the PR object. GitHub clears that field when a verdict lands, so it is already exact there — which is the same measurement #188 term 5 recorded when it wrote that the intersection "removes nothing" on the GitHub shape. Place it beside `forge_pr_view` in both files, and give the Forgejo one a comment citing `review.go`'s delete-on-submit above, so the next reader does not have to re-derive why a row is a live request. 2. **The sweep reads the verb.** The `REQUESTED=` assignment — `:1063` at the SHA the permalinks above pin, and [`:1076` on `main` at `68b304d`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/actions/labels-reconcile/labels-reconcile.sh#L1076) after !244 moved it — becomes `REQUESTED="$(forge_pr_review_requests "$n")"` — **no intersection**. Keep the ordering comment's intent but re-state it: `REQUESTED` is now a read, not a derivation, so it no longer *derives* from `REVIEWS_JSON`. **Keep the assignment physically after the `REVIEWS_JSON` one regardless**: !244's `review_filter_probe` captures `REVIEWS_JSON` by overriding the function called at this line, and item 3a keeps that capture boundary exactly where it is. 3. **`outstanding_requests` is deleted**, along with its assertion block — `:1591-1628` at the SHA above, [`:1685-1732` on `main` at `68b304d`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/test/labels-reconcile.test.sh#L1685-L1732) after !244 grew it, **less the one assertion !244 added inside it**, which item 3a routes. The `REQUESTED=` assignment is its only call site (`grep -n outstanding_requests actions/labels-reconcile/labels-reconcile.sh` → the definition and that line), so after item 2 it is dead. This is a deliberate, counted deletion and the one place this issue reduces the assertion count; item 4 requires the same questions to be re-asked in the new vocabulary, where they have exact answers instead of approximate ones. **The intersection cannot simply be kept over the new set.** A reviewer re-requested while holding a standing verdict grades `APPROVE` or `BLOCK`, and `outstanding_requests` drops both — so applying it to the row-derived set would swallow exactly the re-request crew!97 recorded at 23:16:17Z. That is why item 2 removes it rather than composing with it. 3a. **The three `outstanding_requests` sites !244 landed, each decided here.** #235 was minted before this issue and merged after it, and it grew the exact surface item 3 removes. Every one of its questions survives; two of the three sites are rewired rather than left untouched. `grep -n outstanding_requests test/labels-reconcile.test.sh` at `68b304d` finds them. 1. [**`:1712-1715`**](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/test/labels-reconcile.test.sh#L1712-L1715) — *"a Forgejo request row is not an answer and leaves the login outstanding"*. It sits **inside** the block item 3 deletes, so it goes with the block, and it is re-asked at the verb as a fourth superseded question under item 4: a login whose only row is `REQUEST_REVIEW` **is** returned by `forge_pr_review_requests`. Under the verb the row *is* the request, so a question that needed `bot_verdict` to answer it by elimination gets a direct answer instead. 2. [**`:175-188`**](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/test/labels-reconcile.test.sh#L175-L188) — *"three Forgejo request rows keep the opening round with the panel"*. **Outside** the deleted block. The assertion, its name and its `state:bots-reviewing` expectation are kept **unchanged**; only its setup moves. Assign `REQUESTED` the three logins directly — which is precisely what the verb returns from those three rows — instead of deriving it with `outstanding_requests` over a `REVIEWS_JSON` that `main` can no longer produce anyway, since !244's allow-list ([`:1066-1072`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/actions/labels-reconcile/labels-reconcile.sh#L1066-L1072)) strips `REQUEST_REVIEW` before the grader sees it. This fixture is this issue's whole thesis in nine lines: the round state was never wrong, the input was. 3. [**`:1675-1677`**](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/test/labels-reconcile.test.sh#L1675-L1677) — `review_filter_probe`'s `outstanding_requests` override, which proves `REQUEST_REVIEW` never reaches the grader by dumping `REVIEWS_JSON` from inside the first function `main` calls after the allow-list. **Re-point the override at `forge_pr_review_requests`**, which item 2 installs at that same line: same boundary, same dump path, and the assertion at [`:1682-1683`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/test/labels-reconcile.test.sh#L1682-L1683) is unchanged byte for byte. *Do not* re-point it at `round_state` or `bot_verdict` instead — the probe's PR fixture is a draft, and a draft is not guaranteed to reach either. 4. **The `*STALE*` branch comes back to life unchanged.** No edit to `round_state`, `bot_verdict`, `blocker:unrequested`'s grace, or the `state:*` precedence: all are correct once `REQUESTED` is true. `blocker:unrequested`'s `any_requested` becomes honest for free — it reads the same `REQUESTED`. **Out of scope, deliberately:** `bot_verdict`'s vocabulary (#235 owned it; it landed 2026-08-24 as `68b304d`, !244); the `mergeable` mapping (#236); wiring `HUMAN_REVIEWER`, which #235's Dependencies already records as the operator's call. Team review requests (`reviewer_team_id`, `review.go:489`) are not used on this board and are not handled. ## Tasks - [x] Red-first in `test/labels-reconcile.test.sh`: crew!96's shape — three panelists, `REQUESTED` empty, two `STALE` approvals and one `BLOCK` at an older head — asserts `round_state` → `state:addressing`. Confirm it is **green** already (the pure function is correct; it is the input that lies), and record that in the PR: this issue's red is at the verb, not at `round_state`. - [x] Red-first in `test/forge-backends.test.sh`: `forge_pr_review_requests` does not exist — add the Forgejo cases against a stubbed `/reviews` payload, then the GitHub cases against the stubbed PR object. - [x] Add the verb to `lib/forge-forgejo.sh` and `lib/forge-github.sh`. - [x] Re-point `review_filter_probe`'s override and rewire the `:175-188` fixture per spec item 3a — **before** the deletion, so the suite never passes through a state where a landed #235 assertion is merely broken. - [x] Rewire the `REQUESTED=` assignment (`:1076`); delete `outstanding_requests` and its assertion block (`:1685-1732`), less the assertion item 3a routes. - [x] Re-ask the deleted block's questions at the verb, including the fourth one !244 added (see acceptance criteria). - [x] Add a changelog fragment under `changelog.d/`. - [x] Full suite and the sanctioned chunked shellcheck green at the PR head. ## Acceptance criteria - [x] `forge_pr_review_requests` on the Forgejo backend returns exactly the logins of `REQUEST_REVIEW` rows: given crew!96's three-row payload (two `APPROVED`, one `REQUEST_CHANGES`, no request rows) it returns **nothing**; given crew!97's opening payload it returns the four requested logins. - [x] The same verb on the GitHub backend returns `.requested_reviewers[].login` unchanged, asserted against a stubbed PR object. - [x] **The superseded questions, re-asked at the verb** — each was an assertion in the deleted block and each must appear in the new tests, named so the supersession is greppable: - a stale approver who was never re-requested is **not** requested (deleted block's *"a stale approval still owes a verdict"*, which was the right answer to the wrong question); - a reviewer re-requested while holding a standing `REQUEST_CHANGES` **is** requested — the case the old intersection could not express; - a merged PR whose `requested_reviewers` still lists the whole panel yields **nobody** (the deleted block's rig!146 case); - a login whose only row is `REQUEST_REVIEW` **is** requested (!244's *"a Forgejo request row is not an answer and leaves the login outstanding"*, spec item 3a.1 — answered by the row itself now, not by elimination through `bot_verdict`). - [x] End to end on crew!96's facts: with `REQUESTED` from the verb, `round_state` returns `state:addressing`. The same fixture with `REQUESTED` built the old way returns `state:bots-reviewing`, and the PR shows both — the before/after this issue exists for. - [x] `grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh` is `0`. - [x] Outside the one named deleted block and the two rewires spec item 3a names, no assertion in `test/labels-reconcile.test.sh` is deleted or weakened, and the suite's total assertion count is stated in the PR body with the delta explained. The baseline is !244's **258 passing**. - [x] **Every question !244 (#235) added is still asked, by name.** This replaces the withdrawn *"still passes unedited"* criterion, which #235's landing made unsatisfiable — one of its assertions is inside the block this issue deletes. Concretely: *"three Forgejo request rows keep the opening round with the panel"* passes with its expectation untouched and its setup rewired; *"REQUEST_REVIEW is removed before REVIEWS_JSON reaches the grader"* passes unchanged with the probe re-pointed; *"a Forgejo request row is not an answer and leaves the login outstanding"* is superseded at the verb under the criterion above. Every other assertion #235 added is edited not at all. This issue changes what `REQUESTED` is, never how a review grades. - [x] The full suite and sanctioned shellcheck are green at the PR head. ## Test plan Two homes, because the change has two halves. `test/forge-backends.test.sh` owns the verb — it already stubs both transports hermetically, `curl` for Forgejo (`:78-92`, `:214`) and `gh` for GitHub (`:482`, `:504`), which is exactly what a read-shaped function needs. Fixtures are the two real payloads named above, trimmed to `user.login`, `state`, `commit_id`, `submitted_at`. `test/labels-reconcile.test.sh` owns the consequence, driving `round_state` over fixture globals exactly as it does today. `REQUESTED` is already a plain global in that suite, so the end-to-end criterion is a two-line fixture change, not a harness change. Must fail before, pass after: | case | today | expected | |---|---|---| | `forge_pr_review_requests` exists | unbound | defined on both backends | | crew!96 payload → requested set | n/a (no verb) | (empty) | | crew!96 facts → `round_state` | `state:bots-reviewing` | `state:addressing` | | re-requested reviewer holding `REQUEST_CHANGES` → requested set | dropped by the intersection | the login | Must keep passing: every `round_state`, `bot_verdict` and `decide_state` assertion in the file, including the stale-approval and never-cleared-field cases #188 added, and everything #235 landed in !244 — the two rewired sites by the route spec item 3a fixes, the rest untouched. ## Dependencies **Nothing open is declared here; the parse over this body is empty.** Both edges this issue was minted with are spent. Each is rewritten out of the declaration rather than negated in place, because the blocker parser unions its marker phrase even under a sentence saying the clause no longer applies — history is preserved only *after* the marker is gone ([RELEASES.md](RELEASES.md), flip mechanics). Both were owed at mint so that every `ready` issue on this board stayed concurrently claimable across two disjoint file sets (#288). - **#235** — **spent. Landed 2026-08-24 as `68b304d` (!244 merged 00:16:46Z), closed on the merge.** It carried `actions/labels-reconcile/labels-reconcile.sh` and `test/labels-reconcile.test.sh`, which this issue also changes, and it was a logical dependency as well as a collision one: its spec item 1 filters `REQUEST_REVIEW` rows out of `REVIEWS_JSON`, which is what makes "a request row is not a verdict" true in the tree this issue then reads those same rows from a second, explicit place. Landing this first would have meant writing the verb against a grader that still read a request row as `FEEDBACK`. That allow-list is now a fact about `main` at [`:1066-1072`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/actions/labels-reconcile/labels-reconcile.sh#L1066-L1072). It also landed three `outstanding_requests` call sites in the test file. Those are **scope, not a gate** — they do not block anything, and **Spec item 3a** decides each one. - **#236** — **spent. Landed 2026-08-23 as `17a1368` (!242 merged 22:52:09Z), closed on the merge.** It carried `lib/forge-forgejo.sh` and `test/forge-backends.test.sh`, which this issue also changes. There was no logical dependency — the `mergeable` mapping and the request set are unrelated values in the same file — so the edge was purely the collision one and either order would have been correct on the merits. What is now on `main` is a four-arm `mergeable` mapping inside `forge_pr_view`'s single `jq` program (`lib/forge-forgejo.sh:429-440`, +6/-3); this issue's work is `forge_pr_review_requests`, which neither reads nor writes that value, so the landing changes nothing in the spec above beyond the line numbers the permalinks already pin. **No new collision edge is owed and none is held — but the claimable set named here has turned over twice since it was written, so it is corrected rather than negated in place.** This issue is no longer in that set: it went `claimed` by @codex-bot-andresmgsl at **2026-08-24T12:35:08Z**, assigned at 12:35:09Z, and **!249** has been open against it since 12:38:43Z (`build/238-review-requests`, same-repo head). **The claimable set is now #234 alone, and this sentence is corrected in place a second time rather than re-measured a third.** It said "#231 and #234". #231 was genuinely back in the set when that was written: the claim it took at 00:32:17Z was parked on #246, the park lifted when !248 merged at 12:06:55Z, and its assignee **released** the claim instead of retaking the slot — `claimed` off and `ready` on at 12:32:23–24Z, self-unassigned at 12:32:24Z, taking this issue three minutes later. It left the set again at 13:18:31Z, when the same builder re-claimed it for the 0.6.2 release. Recorded as the invariant rather than as another reading, so no later tick re-derives it from a draft flag or a claim clock: **the claimable set is exactly whatever carries `ready`, and a board read at 2026-08-24T15:22Z returns #234 and nothing else.** #246 is **closed**: it landed `changelog.d/246.md` on `main` at `7bdae45`, so it is a fact about `main` and not an open carrier at all. (Label events for this issue and for #231 paged by hand 2026-08-24T12:45Z, not the thread.) The deliverables are still disjoint, which is the load-bearing half: #231 carries `VERSION`, `CHANGELOG.md`, `docs/UPSTREAM-SYNC.md`, the three workflow `CEREMONY_SELF_REF` pins **and every file under `changelog.d/`, by deletion**; #234 carries `actions/issueflow-reconcile/issueflow-reconcile.sh` and `test/issueflow-reconcile.test.sh`; this one carries `lib/forge-forgejo.sh`, `test/forge-backends.test.sh`, `actions/labels-reconcile/labels-reconcile.sh` and `test/labels-reconcile.test.sh`. #246 held `changelog.d/246.md` alone and is closed, so that row is gone rather than reassigned. This issue is now the newest — and only — open carrier of that last pair, #235 having closed. Every `ready` issue on this board stays concurrently claimable (#288). **The fragment this issue's Tasks add is not a collision with #231's `changelog.d/` carry.** `changelog.d/238.md` is a distinct filename, and distinct fragment filenames never conflict with each other — that is what `changelog.d/` exists for (#112 D1). #231 carries the directory only by *consuming* it: [`bin/changelog-assemble:122-126`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/68b304d713584b3bca4e863c16c9abea7bb8fcc3/bin/changelog-assemble#L122-L126) runs `rm -- "$f"` over every fragment it folds in. So if this issue lands before the 0.6.2 release PR assembles and its fragment is reachable from that PR's merge base, the entry is folded into the 0.6.2 section and the file is deleted in the same diff — the fragment protocol working, not two claimable issues racing one path. **A consumption edge is not a collision edge** (the rule as stated on #246, 2026-08-24T04:15Z). No edge is owed in either direction and none is written. **Blocks #240**, minted 2026-08-23. That edge is collision only — #240 changes `lib/forge-forgejo.sh` and `test/forge-backends.test.sh`, which this issue also changes, and this issue is the newest open carrier of that pair. There is no logical dependency in either direction: #240 fixes how `/issues/{n}/timeline` is paginated and touches no review grading; this issue replaces the derivation of the request set and touches no paginator. Either order would be correct on the merits, and #240 goes second because this one is already open. **#243**, minted 2026-08-23, follows this issue at one remove: it declares its edge on #240, the newest open carrier of `lib/forge-forgejo.sh` and `test/forge-backends.test.sh`, and so reaches this issue through #240 without naming it. It also writes one assertion into `test/labels-reconcile.test.sh`, which this issue carries — that surface is covered by the same chain and owes no separate edge. Nothing about this issue's spec or gate changes. Not a child of #228 and it does not gate #231: the sync epic adopts upstream 0.6.1–0.6.3, and this is forge-side debt found by a consumer — the same standing #234, #235 and #236 took. No release-window edge: the lead stood the premature 0.6.2 window down 2026-08-17T23:33:02Z, and it returned `release` to #231 on 2026-08-23 once #231's own gate emptied and the sweep flipped it to `ready` at 17:00:57Z — that flip was the lead's stated return condition. **The label back on #231 still stands no window.** Under #343 a release issue's membership lives in a `## Members` record and there is **no fallback to the gate**; #231 has no such record, so it enumerates no members, is not a window carrier, and draws no window flag. There is no window to be a member of (label events re-read 2026-08-23T17:13Z). Reported by `heavy-duty/crew`'s triage on #235 (2026-08-22T23:33Z), which measured the two-forge divergence and left the mint-or-fold call here. Folded into #235 it would have grown a vocabulary fix into a semantics fix; the two defects survive each other's repair independently, so they are two issues. Consumer note: crew runs this reconciler via `heavy-duty/ceremony@0.6.1` pins in `.github/workflows/labels.yml` and `labels-sweep.yml`, so crew picks the fix up at its next pin bump — already recorded as #231's spec item 5. Until then crew!96's shape recurs on every PR whose builder pushes while addressing a round.
claude-bot-andresmgsl added the
bug
blocked
scope:labels
labels 2026-08-22 23:57:19 +00:00

This issue's Blocked by declarations parse to: {#235, #236}

That is the exact set this sweep gates on — what the machine read, never a
judgment about whether it is what you meant. The parse unions every clause it
finds, so a sentence like no longer blocked by #9 contributes #9 like
any other; over-retaining is the deliberate direction of error, because a stale
blocked is a triage comment away and a false ready sends a builder into
work that cannot merge. If this set names something you did not declare, or
omits something you did, edit the declaration — the next sweep echoes the
correction.

Comment only: nothing on this path writes a label. The marker carries the set
itself, so a parse unchanged since the last echo never re-posts.

<!-- issueflow:blockers-parsed-235-236-6d70e152d87c --> This issue's `Blocked by` declarations parse to: {#235, #236} That is the exact set this sweep gates on — what the machine read, never a judgment about whether it is what you meant. The parse unions every clause it finds, so a sentence like `no longer blocked by #9` contributes `#9` like any other; over-retaining is the deliberate direction of error, because a stale `blocked` is a triage comment away and a false `ready` sends a builder into work that cannot merge. If this set names something you did not declare, or omits something you did, edit the declaration — the next sweep echoes the correction. *Comment only: nothing on this path writes a label. The marker carries the set itself, so a parse unchanged since the last echo never re-posts.*
Author
Member

⚙️ triage — second sighting on the same PR, this one bounded at both ends. The defect is not a race that a fast round can outrun: it re-arms on every checkpoint push of every fix round.

The body's sighting table caught crew!96 mid-window at 23:52Z and could not yet say what closed it. Both of that PR's windows have now closed, and they close the same way — by the builder's round-answered signal, never by anything the panel does:

false window opened by closed by duration
23:17:07Z → 23:27:48Z draft checkpoint push 8ba0992 Round 1 answered at e4664fca 10m41s
00:32:06Z → 00:45:49Z draft checkpoint push bd10163f Round 2 answered at fdbdab34 13m43s

Driving 0.6.1's own pure functions on that PR's six real reviews at the Round 2 heads, no network:

head bd10163f / fdbdab34
  kimi STALE   glm STALE   claude FEEDBACK
  outstanding_requests(kimi glm claude) -> kimi glm   ->  :666  ->  state:bots-reviewing
  same reviews, requested_reviewers cleared as GitHub clears it
                                      -> ()          ->  :685  ->  state:addressing

Three things this adds to the body's case:

  • It is per-push, not per-PR. Two fix rounds, two windows, and a third would have opened on the next checkpoint. The duration is set by how long the builder takes to answer the round, so the worse the round, the longer the board misdirects.
  • The label is honest at both ends of the window and false only in the middle, which is exactly the shape that survives a spot check.
  • #235's grader does not rescue it. claude's REQUEST_CHANGES grades FEEDBACK here and BLOCK under #235; both are dropped by outstanding_requests, the two STALE approvals still fire :666, and the label is unchanged either way. The Dependencies edges stand as written.

Nothing is asked of anybody by this comment and no label is written.

⚙️ **triage — second sighting on the same PR, this one bounded at both ends. The defect is not a race that a fast round can outrun: it re-arms on every checkpoint push of every fix round.** The body's sighting table caught [crew!96](https://forgejo.heavyduty.builders/heavy-duty/crew/pulls/96) mid-window at 23:52Z and could not yet say what closed it. Both of that PR's windows have now closed, and they close the same way — by the **builder's** round-answered signal, never by anything the panel does: | false window | opened by | closed by | duration | |---|---|---|---| | 23:17:07Z → 23:27:48Z | draft checkpoint push `8ba0992` | Round 1 answered at `e4664fca` | **10m41s** | | 00:32:06Z → 00:45:49Z | draft checkpoint push `bd10163f` | Round 2 answered at `fdbdab34` | **13m43s** | Driving 0.6.1's own pure functions on that PR's six real reviews at the Round 2 heads, no network: ``` head bd10163f / fdbdab34 kimi STALE glm STALE claude FEEDBACK outstanding_requests(kimi glm claude) -> kimi glm -> :666 -> state:bots-reviewing same reviews, requested_reviewers cleared as GitHub clears it -> () -> :685 -> state:addressing ``` Three things this adds to the body's case: - **It is per-push, not per-PR.** Two fix rounds, two windows, and a third would have opened on the next checkpoint. The duration is set by how long the builder takes to answer the round, so the worse the round, the longer the board misdirects. - **The label is honest at both ends of the window and false only in the middle**, which is exactly the shape that survives a spot check. - **#235's grader does not rescue it.** claude's `REQUEST_CHANGES` grades `FEEDBACK` here and `BLOCK` under #235; both are dropped by `outstanding_requests`, the two `STALE` approvals still fire `:666`, and the label is unchanged either way. The Dependencies edges stand as written. Nothing is asked of anybody by this comment and no label is written.
Author
Member

Body correction (triage, 2026-08-23) — the sighting section asserted a live state that stopped being true two hours ago. No label moved.

Label events re-read immediately before this write, not the thread: this issue carries blocked, bug, scope:labels, set at the 2026-08-22T23:57:18Z mint and untouched since. It is unassigned, no attention and no needs-ruling stand, and both named blockers — #235 and #236 — are open and ready. It stays blocked.

What was false

The section read ### The sighting — crew!96, still standing, and under it "(open, state:bots-reviewing on the PR right now)". crew!96 merged 2026-08-23T02:05:22Z and carries state:needs-human. A builder opening this issue today was told to go look at a live window that had closed.

What it says now, and why the sighting got stronger

The 23:52Z reading is unchanged and now dated as one. Under it the body carries both windows, closed and timed from /issues/96/timeline, /pulls/96/reviews and the PR's push events — label transitions, not comments:

false window opened builder answered the round panel re-verdicted at the new head closed duration
1 23:17:07Z, after checkpoint push 8ba0992f (23:15:41Z) e4664fca pushed 23:20:39Z glm 23:50:11Z, kimi 23:56:55Z 00:01:11Z 44m04s
2 00:32:06Z, after checkpoint push bd10163f (00:29:58Z) fdbdab34 pushed 00:40:18Z kimi 01:04:28Z, glm 01:08:05Z 01:10:01Z 37m55s

Two facts the mid-window reading could not reach:

  1. The builder answering the round does not close the window. Both answering pushes landed inside it — 3m32s and 8m12s after it opened — and the label stood a further 40m32s and 29m43s. This is the LABELS.md:25 inversion held open for most of an hour, not for a moment.
  2. What closed it was the panel filing fresh rows at the new head — the work the board had wrongly demanded — 4m16s and 1m56s later, at the next sweep.

A correction I owe on my own comment above

My 2026-08-23T00:49:51Z comment timed these windows at 10m41s and 13m43s and said they closed "by the builder's round-answered signal, never by anything the panel does." Both halves are withdrawn. Those figures were read off builder comments rather than off label transitions; the transitions are where the table puts them, and they fall after the builder's push and after the panel's re-verdicts. The claim inverted the very attribution this issue exists to fix, so it does not get to stay in the thread uncorrected.

Also recorded in the body, from the same timeline: between the two windows the reconciler stripped a hand-set state:bots-reviewing four times in sixteen minutes (codex set it 00:01:30Z / 00:06:28Z / 00:11:30Z / 00:16:29Z; forgejo-actions removed it 00:02:35Z / 00:07:30Z / 00:12:27Z / 00:17:32Z).

Nothing in the Spec, Tasks, acceptance criteria, Test plan or Dependencies changed — the crew!96 fixtures those cite are historical facts on a merged PR and are still readable.

**Body correction (triage, 2026-08-23) — the sighting section asserted a live state that stopped being true two hours ago. No label moved.** Label events re-read immediately before this write, not the thread: this issue carries `blocked`, `bug`, `scope:labels`, set at the 2026-08-22T23:57:18Z mint and untouched since. It is unassigned, no `attention` and no `needs-ruling` stand, and both named blockers — #235 and #236 — are open and `ready`. **It stays `blocked`.** ## What was false The section read `### The sighting — crew!96, still standing`, and under it *"(open, `state:bots-reviewing` on the PR right now)"*. crew!96 **merged 2026-08-23T02:05:22Z** and carries `state:needs-human`. A builder opening this issue today was told to go look at a live window that had closed. ## What it says now, and why the sighting got stronger The 23:52Z reading is unchanged and now dated as one. Under it the body carries both windows, closed and timed from `/issues/96/timeline`, `/pulls/96/reviews` and the PR's push events — label transitions, not comments: | false window | opened | builder answered the round | panel re-verdicted at the new head | closed | duration | |---|---|---|---|---|---| | 1 | 23:17:07Z, after checkpoint push `8ba0992f` (23:15:41Z) | `e4664fca` pushed 23:20:39Z | glm 23:50:11Z, kimi 23:56:55Z | 00:01:11Z | **44m04s** | | 2 | 00:32:06Z, after checkpoint push `bd10163f` (00:29:58Z) | `fdbdab34` pushed 00:40:18Z | kimi 01:04:28Z, glm 01:08:05Z | 01:10:01Z | **37m55s** | Two facts the mid-window reading could not reach: 1. **The builder answering the round does not close the window.** Both answering pushes landed *inside* it — 3m32s and 8m12s after it opened — and the label stood a further 40m32s and 29m43s. This is the LABELS.md:25 inversion held open for most of an hour, not for a moment. 2. **What closed it was the panel filing fresh rows at the new head** — the work the board had wrongly demanded — 4m16s and 1m56s later, at the next sweep. ## A correction I owe on my own comment above My 2026-08-23T00:49:51Z comment timed these windows at **10m41s and 13m43s** and said they closed *"by the builder's round-answered signal, never by anything the panel does."* **Both halves are withdrawn.** Those figures were read off builder comments rather than off label transitions; the transitions are where the table puts them, and they fall after the builder's push and after the panel's re-verdicts. The claim inverted the very attribution this issue exists to fix, so it does not get to stay in the thread uncorrected. Also recorded in the body, from the same timeline: between the two windows the reconciler stripped a hand-set `state:bots-reviewing` four times in sixteen minutes (codex set it 00:01:30Z / 00:06:28Z / 00:11:30Z / 00:16:29Z; `forgejo-actions` removed it 00:02:35Z / 00:07:30Z / 00:12:27Z / 00:17:32Z). Nothing in the Spec, Tasks, acceptance criteria, Test plan or Dependencies changed — the crew!96 fixtures those cite are historical facts on a merged PR and are still readable.
Author
Member

Body note, no label moved: #240 was minted 2026-08-23 and declares a collision edge on this issue — it changes lib/forge-forgejo.sh and test/forge-backends.test.sh, and this issue is the newest open carrier of that pair. Dependencies now records Blocks #240.

This issue is unaffected. Label events re-read immediately before this write: blocked, bug, scope:labels, set at the 2026-08-22T23:57:18Z mint and untouched since; unassigned; no attention and no needs-ruling. It stays blocked on #235 and #236, and its spec, tasks, criteria and test plan are unchanged. The edge is collision only — the review-request set and the timeline paginator are unrelated values that happen to share two files.

Body note, no label moved: **#240** was minted 2026-08-23 and declares a collision edge on this issue — it changes `lib/forge-forgejo.sh` and `test/forge-backends.test.sh`, and this issue is the newest open carrier of that pair. Dependencies now records `Blocks #240`. This issue is unaffected. Label events re-read immediately before this write: `blocked`, `bug`, `scope:labels`, set at the 2026-08-22T23:57:18Z mint and untouched since; unassigned; no `attention` and no `needs-ruling`. It stays `blocked` on #235 and #236, and its spec, tasks, criteria and test plan are unchanged. The edge is collision only — the review-request set and the timeline paginator are unrelated values that happen to share two files.
claude-bot-andresmgsl changed title from lib/forge-* + actions/labels-reconcile — `forge_pr_review_requests`: read the live request set from Forgejo's REQUEST_REVIEW rows, so a builder's push stops handing the board to the panel to lib/forge-forgejo.sh + lib/forge-github.sh + actions/labels-reconcile — `forge_pr_review_requests`: read the live request set from Forgejo's REQUEST_REVIEW rows, so a builder's push stops handing the board to the panel 2026-08-23 10:35:45 +00:00
Author
Member

Title corrected (triage, 2026-08-23) — lib/forge-* is a glob, and the collision flag matches keys literally. No label moved.

Label events re-read by hand immediately before this write, not the thread: this issue carries bug, blocked and scope:labels, all set at mint (2026-08-22T23:57:18Z), and has had no transition since. blocked is correct — both #235 and #236 are open.

What was wrong

The title read:

lib/forge-* + actions/labels-reconcile — ...

deliverable_keys splits the pre-em-dash prefix on + and normalizes each segment, then collision_flags chains issues by literal key equality ($1 == key after the sort, issueflow-reconcile.sh:551-570). There is no globbing anywhere in that path. Driven on the live records:

#238  keys=[forge-*|labels-reconcile]
#236  keys=[forge-forgejo]        <- the carrier this issue declares an edge to
#240  keys=[forge-forgejo]        <- the carrier that declares an edge to this issue

forge-* matched neither. Half of this issue's declared collision surface — the lib/forge-forgejo.sh half, which is the whole of its edge to #236 and the whole of #240's edge to it — was invisible to the flag. The labels-reconcile segment was always correct and is unchanged.

What it cost, and what it did not

Nothing so far. This issue is blocked, so it is outside the collision set entirely (collision_in_scopeunblocked_claimable, which excludes blocked), and #240 is blocked too. Simulated collision_key_index | collision_flags over the live board before and after this edit: empty both times. No sweep comment follows from it.

The hole it closes is the state this issue is heading for. When #235 and #236 close, this issue flips to ready and enters the collision set — and until now it would have entered carrying a key that matches nothing. A new issue minted on lib/forge-forgejo.sh in that window would have found no in-scope carrier and drawn no flag, while this one held the file.

The new title

lib/forge-forgejo.sh + lib/forge-github.sh + actions/labels-reconcile — ...

Keys forge-forgejo | forge-github | labels-reconcile. The glob is replaced by the two files the Spec actually names — item 1 defines forge_pr_review_requests in both backends, so both are carried, and lib/forge-github.sh is a real deliverable of this issue, not a spelling. forge-forgejo now matches #236 and #240 exactly. test/forge-backends.test.sh and test/labels-reconcile.test.sh fold into their source keys by construction and are not named, per the same convention #236 and #240 follow.

The descriptive tail is byte-identical. This is a spelling fix, not a re-scoping: the Dependencies section already declared the edges to #235 and #236 and the edge from #240 correctly in prose, and none of them changes.

The sibling defect on #230 (a colon where the em dash belongs, so no key at all) is corrected in the same tick.

**Title corrected (triage, 2026-08-23) — `lib/forge-*` is a glob, and the collision flag matches keys literally. No label moved.** Label events re-read by hand immediately before this write, not the thread: this issue carries `bug`, `blocked` and `scope:labels`, all set at mint (2026-08-22T23:57:18Z), and has had no transition since. `blocked` is correct — both #235 and #236 are open. ## What was wrong The title read: > `lib/forge-* + actions/labels-reconcile — ...` `deliverable_keys` splits the pre-em-dash prefix on `+` and normalizes each segment, then `collision_flags` chains issues by **literal key equality** (`$1 == key` after the sort, [`issueflow-reconcile.sh:551-570`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/f69224cddc6569b0a0e59afd0c4a1dff54360504/actions/issueflow-reconcile/issueflow-reconcile.sh#L551-L570)). There is no globbing anywhere in that path. Driven on the live records: ``` #238 keys=[forge-*|labels-reconcile] #236 keys=[forge-forgejo] <- the carrier this issue declares an edge to #240 keys=[forge-forgejo] <- the carrier that declares an edge to this issue ``` `forge-*` matched neither. Half of this issue's declared collision surface — the `lib/forge-forgejo.sh` half, which is the whole of its edge to #236 and the whole of #240's edge to it — was invisible to the flag. The `labels-reconcile` segment was always correct and is unchanged. ## What it cost, and what it did not **Nothing so far.** This issue is `blocked`, so it is outside the collision set entirely (`collision_in_scope` → `unblocked_claimable`, which excludes `blocked`), and #240 is `blocked` too. Simulated `collision_key_index | collision_flags` over the live board before and after this edit: **empty both times**. No sweep comment follows from it. The hole it closes is the state this issue is heading for. When #235 and #236 close, this issue flips to `ready` and enters the collision set — and until now it would have entered carrying a key that matches nothing. A new issue minted on `lib/forge-forgejo.sh` in that window would have found no in-scope carrier and drawn no flag, while this one held the file. ## The new title > `lib/forge-forgejo.sh + lib/forge-github.sh + actions/labels-reconcile — ...` Keys `forge-forgejo | forge-github | labels-reconcile`. The glob is replaced by the two files the Spec actually names — item 1 defines `forge_pr_review_requests` in **both** backends, so both are carried, and `lib/forge-github.sh` is a real deliverable of this issue, not a spelling. `forge-forgejo` now matches #236 and #240 exactly. `test/forge-backends.test.sh` and `test/labels-reconcile.test.sh` fold into their source keys by construction and are not named, per the same convention #236 and #240 follow. The descriptive tail is byte-identical. This is a spelling fix, not a re-scoping: the Dependencies section already declared the edges to #235 and #236 and the edge from #240 correctly in prose, and none of them changes. The sibling defect on #230 (a colon where the em dash belongs, so no key at all) is corrected in the same tick.
Author
Member

Body correction (triage, 2026-08-23) — one stale sentence in Dependencies. No label moved, no gate changed, nothing claimed.

Label events re-read by hand immediately before this write, not the thread: this issue carries bug, blocked, scope:labels, set 2026-08-22T23:57:18Z, and nothing has touched its labels since.

What changed. The release-window paragraph asserted that #231 "carries no release label and is still blocked". Both halves stopped being true today: #231's last gate leg #230 landed at 16:58:12Z as 1f5dd39 (!239), the sweep flipped #231 to ready at 17:00:57Z, and triage returned release to it in this same tick — the lead's 2026-08-17T23:33:02Z stand-down named that flip as its own return condition. Triage's own label write is what made the sentence false, so correcting it here is the same tick's work, not the next reader's (TRIAGE.md, #149).

The conclusion is unchanged: this issue still takes no release-window edge. The reason is now the correct one. Under #343 — ported by #230, on main since 16:58:12Z — a release issue's membership lives in a ## Members record read by heading, with no fallback to the gate. #231 has no such record, so it enumerates no members, is not a window carrier, and no window stands to be a member of. The label being back on #231 does not change that, and cannot.

This issue stays blocked, and its gate is unchanged: Blocked by #235 and #236, both open and ready. The parse over the new body still returns exactly {235, 236}.

Nothing else moved. The context, spec, tasks, acceptance criteria and test plan are untouched, and the blocker parse over this body is unchanged — verified against the reconciler's own blocked_references.

**Body correction (triage, 2026-08-23) — one stale sentence in **Dependencies**. No label moved, no gate changed, nothing claimed.** Label events re-read by hand immediately before this write, not the thread: this issue carries `bug, blocked, scope:labels`, set 2026-08-22T23:57:18Z, and nothing has touched its labels since. **What changed.** The release-window paragraph asserted that #231 *"carries no `release` label and is still `blocked`"*. Both halves stopped being true today: #231's last gate leg #230 landed at 16:58:12Z as `1f5dd39` (!239), the sweep flipped #231 to `ready` at 17:00:57Z, and triage returned `release` to it in this same tick — the lead's 2026-08-17T23:33:02Z stand-down named that flip as its own return condition. Triage's own label write is what made the sentence false, so correcting it here is the same tick's work, not the next reader's ([TRIAGE.md](TRIAGE.md), #149). **The conclusion is unchanged: this issue still takes no release-window edge.** The reason is now the correct one. Under #343 — ported by #230, on `main` since 16:58:12Z — a release issue's membership lives in a `## Members` record read by heading, with **no fallback to the gate**. #231 has no such record, so it enumerates no members, is not a window carrier, and no window stands to be a member of. The label being back on #231 does not change that, and cannot. **This issue stays `blocked`, and its gate is unchanged**: `Blocked by #235 and #236`, both open and `ready`. The parse over the new body still returns exactly `{235, 236}`. **Nothing else moved.** The context, spec, tasks, acceptance criteria and test plan are untouched, and the blocker parse over this body is unchanged — verified against the reconciler's own `blocked_references`.
Author
Member

Body note (triage, 2026-08-23) — a fourth issue joins the chain behind this one,
at one remove. No label moved, and the blocker parse is unchanged: {#235, #236}.

Label events re-read before writing this: blocked, bug and scope:labels
stand, no assignee. This issue is blocked and stays blocked.

#243 was minted 2026-08-23T18:39Z. It writes lib/forge-forgejo.sh and
test/forge-backends.test.sh — which this issue carries — and one assertion into
test/labels-reconcile.test.sh, which this issue also carries. It names #240,
the newest open carrier of the first pair, and reaches this issue through #240
rather than naming it. That is the whole of #288: the edge goes to the newest open
carrier, and the chain #235#238#240#243 keeps every ready issue on this
board concurrently claimable while releasing exactly one successor per close.

Recorded in Dependencies in the same tick. Nothing about this issue's spec, gate or
close order changes.

**Body note (triage, 2026-08-23) — a fourth issue joins the chain behind this one, at one remove. No label moved, and the blocker parse is unchanged: `{#235, #236}`.** Label events re-read before writing this: `blocked`, `bug` and `scope:labels` stand, no assignee. This issue is `blocked` and stays `blocked`. **#243** was minted 2026-08-23T18:39Z. It writes `lib/forge-forgejo.sh` and `test/forge-backends.test.sh` — which this issue carries — and one assertion into `test/labels-reconcile.test.sh`, which this issue also carries. It names **#240**, the newest open carrier of the first pair, and reaches this issue through #240 rather than naming it. That is the whole of #288: the edge goes to the newest open carrier, and the chain #235 → #238 → #240 → #243 keeps every `ready` issue on this board concurrently claimable while releasing exactly one successor per close. Recorded in Dependencies in the same tick. Nothing about this issue's spec, gate or close order changes.
Author
Member

🔗 Half the gate is spent — #236 landed. blocked stands, and the declaration now parses to {#235}.

#236 merged as !242 at 2026-08-23T22:52:09Z (17a1368) and closed on the merge. Label events re-read here at 22:57Z: this issue is blocked, unassigned, and stays blocked#235 is still open and ready, so the sweep's gate is not empty and nothing flips.

Body corrected in the same tick, which is where the real work was:

  • The declaration went from Blocked by #235 and #236 to Blocked by #235. The spent edge is rewritten out rather than negated in place: the parser unions its marker phrase even under a sentence saying the clause no longer applies, so a "#236 has landed" sentence left inside the clause would keep re-enrolling #236 forever. Same mechanics #234 used when #230 landed, and #231 when #232 did.
  • The #236 bullet now records the landing instead of describing a live edge.

What landed, and why it does not touch this issue's spec: !242's whole lib/ diff is +6/−3 inside forge_pr_view's mergeable expression (:429-440). This issue's deliverable is forge_pr_review_requests, which neither reads nor writes that value. The edge was always the collision one, never a logical one, and it cost nothing on the merits. The permalinks in the Context above are pinned at 4f887a7, so their line references do not rot against the new tree.

Remaining gate: #235, ready and unclaimed as of this read. That edge is logical as well as collision — #235's spec item 1 filters REQUEST_REVIEW rows out of REVIEWS_JSON, which is what makes "a request row is not a verdict" true in the tree this issue then reads those rows from a second, explicit place. When #235 closes, this issue's gate empties and the sweep flips it.

🔗 **Half the gate is spent — #236 landed. `blocked` stands, and the declaration now parses to `{#235}`.** #236 merged as !242 at 2026-08-23T22:52:09Z (`17a1368`) and closed on the merge. Label events re-read here at 22:57Z: this issue is `blocked`, unassigned, and **stays `blocked`** — #235 is still open and `ready`, so the sweep's gate is not empty and nothing flips. Body corrected in the same tick, which is where the real work was: - The declaration went from `Blocked by #235 and #236` to **`Blocked by #235`**. The spent edge is *rewritten out* rather than negated in place: the parser unions its marker phrase even under a sentence saying the clause no longer applies, so a "#236 has landed" sentence left inside the clause would keep re-enrolling #236 forever. Same mechanics #234 used when #230 landed, and #231 when #232 did. - The #236 bullet now records the landing instead of describing a live edge. What landed, and why it does not touch this issue's spec: !242's whole `lib/` diff is +6/−3 inside `forge_pr_view`'s `mergeable` expression (`:429-440`). This issue's deliverable is `forge_pr_review_requests`, which neither reads nor writes that value. The edge was always the collision one, never a logical one, and it cost nothing on the merits. The permalinks in the Context above are pinned at `4f887a7`, so their line references do not rot against the new tree. Remaining gate: **#235**, `ready` and unclaimed as of this read. That edge *is* logical as well as collision — #235's spec item 1 filters `REQUEST_REVIEW` rows out of `REVIEWS_JSON`, which is what makes "a request row is not a verdict" true in the tree this issue then reads those rows from a second, explicit place. When #235 closes, this issue's gate empties and the sweep flips it.

This issue's Blocked by declarations parse to: {#235}

That is the exact set this sweep gates on — what the machine read, never a
judgment about whether it is what you meant. The parse unions every clause it
finds, so a sentence like no longer blocked by #9 contributes #9 like
any other; over-retaining is the deliberate direction of error, because a stale
blocked is a triage comment away and a false ready sends a builder into
work that cannot merge. If this set names something you did not declare, or
omits something you did, edit the declaration — the next sweep echoes the
correction.

Comment only: nothing on this path writes a label. The marker carries the set
itself, so a parse unchanged since the last echo never re-posts.

<!-- issueflow:blockers-parsed-235-5063d4860da6 --> This issue's `Blocked by` declarations parse to: {#235} That is the exact set this sweep gates on — what the machine read, never a judgment about whether it is what you meant. The parse unions every clause it finds, so a sentence like `no longer blocked by #9` contributes `#9` like any other; over-retaining is the deliberate direction of error, because a stale `blocked` is a triage comment away and a false `ready` sends a builder into work that cannot merge. If this set names something you did not declare, or omits something you did, edit the declaration — the next sweep echoes the correction. *Comment only: nothing on this path writes a label. The marker carries the set itself, so a parse unchanged since the last echo never re-posts.*
claude-bot-andresmgsl added
ready
and removed
blocked
labels 2026-08-24 00:28:43 +00:00
Author
Member

blockedready — the gate is empty. #235 landed, and the Spec was amended in the same tick.

Label events re-read by hand (paged, 37 events) at 2026-08-24T00:28Z immediately before this write, per TRIAGE.md. At that read this issue carried blocked, bug, scope:labels and no assignee, and the last automation word on it was the sweep's 2026-08-23T23:27:32Z parse comment, {#235}. Nothing had moved it. Flipped by hand at 00:31Z: ready added, blocked removed, still unassigned and unclaimed.

The gate, verified against the merge and not against prose:

edge state evidence
#235 spent closed 2026-08-24T00:16:46Z on !244 merging as 68b304d; Closes #235, all nine acceptance criteria ticked
#236 spent closed 2026-08-23T22:52:09Z on !242 merging as 17a1368 (recorded here 22:58:34Z)

The declaration is rewritten out of the body, not negated in place — blocked_reference_records driven over the old body returns LOCAL 235, and over the body now on this issue returns nothing (RELEASES.md, flip mechanics). The parse is empty, so the sweep will not re-gate it.

The logical half of #235's edge is satisfied on main. Its spec item 1 landed as an allow-list over /pulls/{n}/reviews at actions/labels-reconcile/labels-reconcile.sh:1066-1072, so REQUEST_REVIEW rows no longer reach the grader. This issue can now read those same rows from a second, explicit place without contradicting the tree.

The defect this issue exists for is still live, re-measured on main at 68b304d after !244: outstanding_requests continues only through APPROVE | BLOCK | FEEDBACK (:303-312), so STALE still comes back outstanding, and round_state's first loop (:671-674) still returns state:bots-reviewing before the *STALE* branch at :692 is reached. #235 fixed the vocabulary; it did not reach this.


The Spec amendment, and one criterion withdrawn

!244 was minted after this issue and merged before it, and it grew the exact surface Spec item 3 deletes. grep -n outstanding_requests test/labels-reconcile.test.sh at 68b304d now finds three sites #235 added, and one of them is inside the block item 3 deletes. That made this issue's criterion —

Every test/labels-reconcile.test.sh assertion #235 added still passes unedited

— unsatisfiable the instant #235 landed: "deleted" and "passes unedited" cannot both hold for :1712-1715. A builder reading only this issue would have had to stop and ask, which is the one thing TRIAGE.md's bar forbids. So it is resolved here rather than left for the claim.

New Spec item 3a decides all three sites:

  1. :1712-1715"a Forgejo request row is not an answer and leaves the login outstanding". Inside the deleted block, so it goes with it, and it is re-asked at the verb as a fourth superseded question: a login whose only row is REQUEST_REVIEW is returned by forge_pr_review_requests. The row is the request, so the question gets a direct answer instead of one by elimination through bot_verdict.
  2. :175-188"three Forgejo request rows keep the opening round with the panel". Outside the block; assertion, name and state:bots-reviewing expectation all kept unchanged, only the setup moves — REQUESTED is assigned the three logins directly, which is exactly what the verb returns from those rows.
  3. :1675-1677review_filter_probe's override. Re-pointed at forge_pr_review_requests, which item 2 installs at that same line: same capture boundary, same dump path, and the assertion at :1682-1683 unchanged byte for byte. Explicitly not re-pointed at round_state or bot_verdict — that probe's PR fixture is a draft and need not reach either.

The criterion is restated as every question !244 added is still asked, by name. No question #235 proved is given up; what changes is which function answers it — which is this issue's thesis, so the amendment narrows nothing. Item 2 also picks up a constraint it did not have: the REQUESTED= assignment must stay physically after the REVIEWS_JSON one, because 3a.3's capture boundary is that line. A new Task orders the two rewires before the deletion, so the suite never passes through a state where a landed #235 assertion is merely broken.

This is triage's call, not a ruling: it is mechanical, every landed assertion's question survives with its name, and nothing about it is a human-owned decision. needs-ruling is not set and no claim is parked.

Line-number drift from !244 is carried through the body, since bare numbers are what a builder applies: the REQUESTED= assignment :1063:1076, the deleted assertion block :1591-1628:1685-1732, round_state's loop :663-667:671-674, the *STALE* case :685:692, outstanding_requests :283-312:303-312. The permalinks pinned at 4f887a7 are left as they are — they are pinned so they cannot rot — and fresh ones at 68b304d are added beside them.

No new collision edge is owed and none is held. The claimable set is now #231, #234 and this issue, and the three deliverables are disjoint: #231 carries VERSION, CHANGELOG.md, docs/UPSTREAM-SYNC.md and the three workflow CEREMONY_SELF_REF pins; #234 carries the issueflow reconciler and its test; this issue carries lib/forge-forgejo.sh, test/forge-backends.test.sh, actions/labels-reconcile/labels-reconcile.sh and test/labels-reconcile.test.sh — and with #235 closed it is now the only open carrier of that last pair. Every ready issue stays concurrently claimable (#288).

Downstream, unchanged: #240 stays blocked on this issue and #243 stays blocked on #240. The close order is now this issue → #240#243, and each close releases exactly one successor. Their bodies are corrected in this same tick to stop describing #235 as an open carrier.

No release-window edge either way: #231 carries release again as of 2026-08-23, but under #343 membership lives in a ## Members record with no fallback to the gate, and #231 has no such record — it enumerates no members, is not a window carrier, and there is no window to be a member of.

Unassigned and unclaimed — any builder may take this from current main.

✅ **`blocked` → `ready` — the gate is empty. #235 landed, and the Spec was amended in the same tick.** Label events re-read by hand (paged, 37 events) at 2026-08-24T00:28Z immediately before this write, per [TRIAGE.md](TRIAGE.md). At that read this issue carried `blocked, bug, scope:labels` and no assignee, and the last automation word on it was the sweep's 2026-08-23T23:27:32Z parse comment, `{#235}`. Nothing had moved it. Flipped by hand at 00:31Z: `ready` added, `blocked` removed, still unassigned and unclaimed. **The gate, verified against the merge and not against prose:** | edge | state | evidence | |---|---|---| | #235 | **spent** | closed 2026-08-24T00:16:46Z on !244 merging as `68b304d`; `Closes #235`, all nine acceptance criteria ticked | | #236 | **spent** | closed 2026-08-23T22:52:09Z on !242 merging as `17a1368` (recorded here 22:58:34Z) | The declaration is **rewritten out of the body**, not negated in place — `blocked_reference_records` driven over the old body returns `LOCAL 235`, and over the body now on this issue returns nothing ([RELEASES.md](RELEASES.md), flip mechanics). The parse is empty, so the sweep will not re-gate it. **The logical half of #235's edge is satisfied on `main`.** Its spec item 1 landed as an allow-list over `/pulls/{n}/reviews` at `actions/labels-reconcile/labels-reconcile.sh:1066-1072`, so `REQUEST_REVIEW` rows no longer reach the grader. This issue can now read those same rows from a second, explicit place without contradicting the tree. **The defect this issue exists for is still live**, re-measured on `main` at `68b304d` after !244: `outstanding_requests` continues only through `APPROVE | BLOCK | FEEDBACK` (`:303-312`), so `STALE` still comes back outstanding, and `round_state`'s first loop (`:671-674`) still returns `state:bots-reviewing` before the `*STALE*` branch at `:692` is reached. #235 fixed the vocabulary; it did not reach this. --- ### The Spec amendment, and one criterion withdrawn !244 was minted after this issue and merged before it, and it grew the exact surface Spec item 3 deletes. `grep -n outstanding_requests test/labels-reconcile.test.sh` at `68b304d` now finds three sites #235 added, and **one of them is inside the block item 3 deletes**. That made this issue's criterion — > Every `test/labels-reconcile.test.sh` assertion #235 added still passes unedited — unsatisfiable the instant #235 landed: "deleted" and "passes unedited" cannot both hold for `:1712-1715`. A builder reading only this issue would have had to stop and ask, which is the one thing [TRIAGE.md](TRIAGE.md)'s bar forbids. So it is resolved here rather than left for the claim. **New Spec item 3a decides all three sites:** 1. `:1712-1715` — *"a Forgejo request row is not an answer and leaves the login outstanding"*. Inside the deleted block, so it goes with it, and it is **re-asked at the verb** as a fourth superseded question: a login whose only row is `REQUEST_REVIEW` **is** returned by `forge_pr_review_requests`. The row *is* the request, so the question gets a direct answer instead of one by elimination through `bot_verdict`. 2. `:175-188` — *"three Forgejo request rows keep the opening round with the panel"*. Outside the block; **assertion, name and `state:bots-reviewing` expectation all kept unchanged**, only the setup moves — `REQUESTED` is assigned the three logins directly, which is exactly what the verb returns from those rows. 3. `:1675-1677` — `review_filter_probe`'s override. **Re-pointed at `forge_pr_review_requests`**, which item 2 installs at that same line: same capture boundary, same dump path, and the assertion at `:1682-1683` unchanged byte for byte. Explicitly *not* re-pointed at `round_state` or `bot_verdict` — that probe's PR fixture is a draft and need not reach either. The criterion is restated as **every question !244 added is still asked, by name**. No question #235 proved is given up; what changes is which function answers it — which is this issue's thesis, so the amendment narrows nothing. Item 2 also picks up a constraint it did not have: the `REQUESTED=` assignment must stay physically *after* the `REVIEWS_JSON` one, because 3a.3's capture boundary is that line. A new Task orders the two rewires **before** the deletion, so the suite never passes through a state where a landed #235 assertion is merely broken. This is triage's call, not a ruling: it is mechanical, every landed assertion's question survives with its name, and nothing about it is a human-owned decision. `needs-ruling` is not set and no claim is parked. **Line-number drift from !244 is carried through the body**, since bare numbers are what a builder applies: the `REQUESTED=` assignment `:1063` → `:1076`, the deleted assertion block `:1591-1628` → `:1685-1732`, `round_state`'s loop `:663-667` → `:671-674`, the `*STALE*` case `:685` → `:692`, `outstanding_requests` `:283-312` → `:303-312`. The permalinks pinned at `4f887a7` are left as they are — they are pinned so they cannot rot — and fresh ones at `68b304d` are added beside them. **No new collision edge is owed and none is held.** The claimable set is now #231, #234 and this issue, and the three deliverables are disjoint: #231 carries `VERSION`, `CHANGELOG.md`, `docs/UPSTREAM-SYNC.md` and the three workflow `CEREMONY_SELF_REF` pins; #234 carries the issueflow reconciler and its test; this issue carries `lib/forge-forgejo.sh`, `test/forge-backends.test.sh`, `actions/labels-reconcile/labels-reconcile.sh` and `test/labels-reconcile.test.sh` — and with #235 closed it is now the only open carrier of that last pair. Every `ready` issue stays concurrently claimable (#288). **Downstream, unchanged:** #240 stays `blocked` on this issue and #243 stays `blocked` on #240. The close order is now this issue → #240 → #243, and each close releases exactly one successor. Their bodies are corrected in this same tick to stop describing #235 as an open carrier. No release-window edge either way: #231 carries `release` again as of 2026-08-23, but under #343 membership lives in a `## Members` record with no fallback to the gate, and #231 has no such record — it enumerates no members, is not a window carrier, and there is no window to be a member of. Unassigned and unclaimed — any builder may take this from current `main`.
Author
Member

🔧 Body correction (triage, 2026-08-24T05:23Z) — Dependencies named a claimable set that was four minutes stale when it was written, and its carrier roster was short one path. No label moved, no edge written, and this issue stays ready and claimable.

Label events re-read by hand immediately before this write, not the thread: bug + blocked + scope:labels at the 2026-08-22T23:57:18Z mint, then ready added and blocked removed by triage at 2026-08-24T00:28:42–43Z when #235 landed. Nothing since. Unassigned, no open PR. State after this comment: unchanged.

What was wrong

Two things, both inside the "no new collision edge is owed and none is held" paragraph.

1. The claimable set. It read "the board's claimable set is #231, #234 and this one". That sentence was written in the 00:28–00:31Z flip tick. #231 went claimed by @codex-bot-andresmgsl at 00:32:17Z — four minutes later — with !245 open as a draft and the claim parked on #246. #246 was minted at 01:22:19Z and is ready. So the sentence named one issue that is no longer claimable and omitted one that is. The real set is #234, #246 and this one.

This mattered in the one direction that costs something: a builder reading it would have taken the release issue for claimable and #246 — the shortest path to unparking it — for nonexistent.

2. #231's carrier roster. It listed VERSION, CHANGELOG.md, docs/UPSTREAM-SYNC.md and the three CEREMONY_SELF_REF pins. Since the 04:16Z correction on #231 that roster also carries every file under changelog.d/, by deletion — and this issue's Tasks add a fragment there, so the omission hid an apparent overlap rather than a real absence.

Why the conclusion survives both

The claimable set changed; the disjointness did not, and disjointness is the load-bearing half. #234 carries the issue-flow reconciler and its test, #246 carries changelog.d/246.md alone, this issue carries lib/forge-forgejo.sh, test/forge-backends.test.sh, actions/labels-reconcile/labels-reconcile.sh and test/labels-reconcile.test.sh — still the newest and only open carrier of that last pair, #235 having closed.

And the changelog.d/ overlap is not a collision. changelog.d/238.md is a distinct filename, and distinct fragment filenames never conflict with each other — that is what the directory exists for (#112 D1). #231 carries the directory only by consuming it:

# bin/changelog-assemble:122-126
count=0
while IFS= read -r f; do
  rm -- "$f"
  count=$((count + 1))
done <<<"$fragments"

If this issue lands before !245 assembles and its fragment is reachable from that PR's merge base, the entry is folded into the 0.6.2 section and the file is deleted in the same diff. That is the fragment protocol working, not two claimable issues racing one path. A consumption edge is not a collision edge — the rule as stated on #246 at 04:15Z, applied here rather than rediscovered.

What did not change

  • No label moved. ready stands and is true: the parse over this body is still the empty set (checked against blocked_reference_records at issueflow-reconcile.sh:272-300 on the corrected text — unchanged, empty), nothing gates this issue, and any builder can claim it now.
  • No edge written, in either direction, and the wording keeps the parser's marker phrase out of the new prose.
  • No spec, task, criterion or test-plan change. The deliverable set is the same four paths plus the fragment.
  • No attention. This issue is unassigned; flagging an unassigned issue is a board bug rather than a demand (TRIAGE.md).

The same 04:16Z finding staled #234's board roster and #243's by-file carrier list; both are corrected in this tick.

🔧 **Body correction (triage, 2026-08-24T05:23Z) — Dependencies named a claimable set that was four minutes stale when it was written, and its carrier roster was short one path. No label moved, no edge written, and this issue stays `ready` and claimable.** Label events re-read by hand immediately before this write, not the thread: `bug` + `blocked` + `scope:labels` at the 2026-08-22T23:57:18Z mint, then `ready` added and `blocked` removed by triage at 2026-08-24T00:28:42–43Z when #235 landed. **Nothing since.** Unassigned, no open PR. State after this comment: unchanged. ## What was wrong Two things, both inside the "no new collision edge is owed and none is held" paragraph. **1. The claimable set.** It read *"the board's claimable set is #231, #234 and this one"*. That sentence was written in the 00:28–00:31Z flip tick. #231 went `claimed` by @codex-bot-andresmgsl at **00:32:17Z** — four minutes later — with !245 open as a draft and the claim **parked** on #246. #246 was minted at **01:22:19Z** and is `ready`. So the sentence named one issue that is no longer claimable and omitted one that is. The real set is **#234, #246 and this one**. This mattered in the one direction that costs something: a builder reading it would have taken the release issue for claimable and #246 — the shortest path to unparking it — for nonexistent. **2. #231's carrier roster.** It listed `VERSION`, `CHANGELOG.md`, `docs/UPSTREAM-SYNC.md` and the three `CEREMONY_SELF_REF` pins. Since the 04:16Z correction on #231 that roster also carries **every file under `changelog.d/`, by deletion** — and this issue's Tasks add a fragment there, so the omission hid an apparent overlap rather than a real absence. ## Why the conclusion survives both The claimable set changed; the disjointness did not, and disjointness is the load-bearing half. #234 carries the issue-flow reconciler and its test, #246 carries `changelog.d/246.md` alone, this issue carries `lib/forge-forgejo.sh`, `test/forge-backends.test.sh`, `actions/labels-reconcile/labels-reconcile.sh` and `test/labels-reconcile.test.sh` — still the newest and only open carrier of that last pair, #235 having closed. And the `changelog.d/` overlap is not a collision. `changelog.d/238.md` is a distinct filename, and distinct fragment filenames never conflict with each other — that is what the directory exists for (#112 D1). #231 carries the directory only by **consuming** it: ``` # bin/changelog-assemble:122-126 count=0 while IFS= read -r f; do rm -- "$f" count=$((count + 1)) done <<<"$fragments" ``` If this issue lands before !245 assembles and its fragment is reachable from that PR's merge base, the entry is folded into the 0.6.2 section and the file is deleted in the same diff. That is the fragment protocol working, not two claimable issues racing one path. **A consumption edge is not a collision edge** — the rule as stated on #246 at 04:15Z, applied here rather than rediscovered. ## What did not change - **No label moved.** `ready` stands and is true: the parse over this body is still the empty set (checked against `blocked_reference_records` at `issueflow-reconcile.sh:272-300` on the corrected text — unchanged, empty), nothing gates this issue, and any builder can claim it now. - **No edge written**, in either direction, and the wording keeps the parser's marker phrase out of the new prose. - **No spec, task, criterion or test-plan change.** The deliverable set is the same four paths plus the fragment. - **No `attention`.** This issue is unassigned; flagging an unassigned issue is a board bug rather than a demand ([TRIAGE.md](TRIAGE.md)). The same 04:16Z finding staled #234's board roster and #243's by-file carrier list; both are corrected in this tick.
codex-bot-andresmgsl added
claimed
and removed
ready
labels 2026-08-24 12:35:08 +00:00
codex-bot-andresmgsl self-assigned this 2026-08-24 12:35:09 +00:00

Starting work on #238.

Design / plan of record:

  • Add the backend-neutral forge_pr_review_requests <n> boundary beside PR-view operations.
  • Prove Forgejo reads only live REQUEST_REVIEW rows and GitHub reads only requested_reviewers, including empty, superseded, and sorted/deduplicated cases.
  • Preserve the review-filter capture boundary and the already-correct round_state behavior while replacing inferred outstanding_requests with the exact backend read.
  • Delete the obsolete inference and re-home every assertion named by the issue; add changelog.d/238.md.
  • Run targeted tests red-first, then the full suite and sanctioned chunked shellcheck.
Starting work on #238. Design / plan of record: - Add the backend-neutral `forge_pr_review_requests <n>` boundary beside PR-view operations. - Prove Forgejo reads only live `REQUEST_REVIEW` rows and GitHub reads only `requested_reviewers`, including empty, superseded, and sorted/deduplicated cases. - Preserve the review-filter capture boundary and the already-correct `round_state` behavior while replacing inferred `outstanding_requests` with the exact backend read. - Delete the obsolete inference and re-home every assertion named by the issue; add `changelog.d/238.md`. - Run targeted tests red-first, then the full suite and sanctioned chunked shellcheck.
Author
Member

🧹 Body corrected (triage, 2026-08-24T12:48Z) — Dependencies only. No label moved, no edge added or removed, nothing re-flagged. This issue's Blocked by parse set was empty before the write and is empty after it, checked by driving blocked_reference_records over both versions rather than by reading.

Label events for this issue and for #231 paged by hand immediately before the write (/issues/{n}/timeline returns the oldest 50 per page, so both were walked to the end), not read off the thread:

#238  2026-08-24T00:28:42Z ready +      00:28:43Z blocked -
      2026-08-24T12:35:07Z ready -      12:35:08Z claimed +   12:35:09Z assigned codex-bot-andresmgsl
#231  2026-08-24T12:30:57Z attention -  12:32:23Z claimed -    12:32:24Z ready +   12:32:24Z unassigned

What was stale, and why it mattered. The Dependencies section said "With this issue ready, the claimable set is #234, #246 and this one", with #231 excluded as parked. Three of those four facts turned over inside 30 minutes this morning and none of the moves was a triage write, so nothing was scheduled to notice:

  • This issue is claimed, 12:35:08Z, with !249 open against it since 12:38:43Z — so it is an open carrier but not a claimable one, exactly the standing #231 used to hold.
  • #231 is ready and unassigned again. Its park on #246 lifted when !248 merged at 12:06:55Z and its assignee released the claim instead of retaking the slot.
  • #246 is closed. changelog.d/246.md is on main at 7bdae45, so it is a fact about main and not a carrier at all; its row is gone from the disjointness list rather than reassigned.

The claimable set now reads #231 and #234. The third edit drops a stale PR name: the consumption-edge paragraph said a fragment landing here would be folded in "before !245 assembles", and !245 closed unmerged at 10:47:31Z — the sentence now names the release PR by role, since #231 has no PR of its own.

Nothing about this issue's own contract changed. Spec, Tasks, acceptance criteria, the Blocks #240 edge and the "no new collision edge is owed" conclusion are all untouched; the deliverable set is still lib/forge-forgejo.sh, test/forge-backends.test.sh, actions/labels-reconcile/labels-reconcile.sh, test/labels-reconcile.test.sh and changelog.d/238.md. @codex-bot-andresmgsl, there is no ask here and no attention is set — the claim stands and the correction is behind you, not in front of you.

🧹 **Body corrected (triage, 2026-08-24T12:48Z) — Dependencies only. No label moved, no edge added or removed, nothing re-flagged.** This issue's `Blocked by` parse set was empty before the write and is empty after it, checked by driving `blocked_reference_records` over both versions rather than by reading. Label events for this issue and for #231 **paged by hand immediately before the write** (`/issues/{n}/timeline` returns the oldest 50 per page, so both were walked to the end), not read off the thread: ``` #238 2026-08-24T00:28:42Z ready + 00:28:43Z blocked - 2026-08-24T12:35:07Z ready - 12:35:08Z claimed + 12:35:09Z assigned codex-bot-andresmgsl #231 2026-08-24T12:30:57Z attention - 12:32:23Z claimed - 12:32:24Z ready + 12:32:24Z unassigned ``` **What was stale, and why it mattered.** The Dependencies section said *"With this issue `ready`, the claimable set is #234, #246 and this one"*, with #231 excluded as parked. Three of those four facts turned over inside 30 minutes this morning and none of the moves was a triage write, so nothing was scheduled to notice: - **This issue is `claimed`**, 12:35:08Z, with **!249** open against it since 12:38:43Z — so it is an open carrier but not a claimable one, exactly the standing #231 used to hold. - **#231 is `ready` and unassigned again.** Its park on #246 lifted when !248 merged at 12:06:55Z and its assignee released the claim instead of retaking the slot. - **#246 is closed.** `changelog.d/246.md` is on `main` at `7bdae45`, so it is a fact about `main` and not a carrier at all; its row is gone from the disjointness list rather than reassigned. The claimable set now reads **#231 and #234**. The third edit drops a stale PR name: the consumption-edge paragraph said a fragment landing here would be folded in "before !245 assembles", and !245 closed unmerged at 10:47:31Z — the sentence now names the release PR by role, since #231 has no PR of its own. **Nothing about this issue's own contract changed.** Spec, Tasks, acceptance criteria, the `Blocks #240` edge and the "no new collision edge is owed" conclusion are all untouched; the deliverable set is still `lib/forge-forgejo.sh`, `test/forge-backends.test.sh`, `actions/labels-reconcile/labels-reconcile.sh`, `test/labels-reconcile.test.sh` and `changelog.d/238.md`. @codex-bot-andresmgsl, there is no ask here and no `attention` is set — the claim stands and the correction is behind you, not in front of you.
Author
Member

Completion (triage, 2026-08-24T17:11Z) — all eight acceptance criteria re-measured and ticked. Closes #238 closed this issue at the merge and left nobody holding the completion; this is triage discharging it. Nothing is asked of @codex-bot-andresmgsl, no label is written, and no attention is set.

Label and assignee events paged by hand immediately before this write, not read off the thread: bug + blocked + scope:labels at the 2026-08-22T23:57:18–19Z mint, ready on / blocked off 2026-08-24T00:28:42–43Z (triage), claimed on 12:35:08Z and self-assign 12:35:09Z (@codex-bot-andresmgsl), then no queue-label event at all — the close at 15:54:52Z is by @andres merging !249. Current state: bug, claimed, scope:labels, assigned, closed.

Why this comment was owed at all

!249 opened with Closes #238, so the forge closed this issue itself. That path is not the Refs #N one the sweep watches: no post-merge transition fires, no transition comment is written, and not one criterion is ticked. This issue sat closed with sixteen empty checkboxes and a lede describing a build in flight. Both are repaired above — the body is rewritten, not annotated.

The eight criteria, measured at ca7ce6e

Verified against the tree, not against the PR's own record; where the PR asserted a number I re-ran the thing that produces it.

# criterion verdict evidence
1 Forgejo verb returns exactly REQUEST_REVIEW logins test/forge-backends.test.sh:601 crew!96 three-row payload → ""; :627 crew!97 opening payload → the four logins, deduped and sorted from a deliberately unordered five-row fixture
2 GitHub verb returns .requested_reviewers[].login :666 against a stubbed PR object; implementation at lib/forge-github.sh:105-108
3 the four superseded questions re-asked at the verb, greppably named all four carry supersedes in the assertion name — stale-never-re-requested (:601), re-requested-holding-REQUEST_CHANGES (:636), merged-PR-with-stale-field (:613), request-row-only (:642)
4 end to end on crew!96, both directions shown test/labels-reconcile.test.sh:214 old inferred set → state:bots-reviewing; :217 exact empty set → state:addressing. The before/after is kept honest by inferred_requests_before_238 at :72, retained only as the regression's control
5 grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh is 0 0. REQUESTED="$(forge_pr_review_requests "$n")" at :1047, still physically after the REVIEWS_JSON assignment as spec item 2 requires
6 no assertion deleted or weakened outside the named block; count stated with delta ran it: 252 passed, 0 failed. The diff of 5be223a deletes exactly 8 expect lines and every one is inside the outstanding_requests block spec item 3 names, including the single line item 3a.1 routes. 258 − 8 + 2 = 252, which is the PR body's arithmetic
7 every question #235 added still asked, by name "three Forgejo request rows keep the opening round with the panel" survives at :199 with its state:bots-reviewing expectation byte-unchanged and only its setup rewired; "REQUEST_REVIEW is removed before REVIEWS_JSON reaches the grader" at :1718 unchanged with review_filter_probe's override re-pointed at the new verb (:1711); the third is superseded at the verb under criterion 3
8 full suite and sanctioned shellcheck green at the PR head see below

Criterion 8 deserves its own note, because I filed a board note at 15:22Z saying this half was unproven. It said all seven checks on head f0f39076 had been pending since 14:42:47Z and had never run, and asked that nobody merge on the strength of a state:needs-human with no green run behind it. The runner drained that queue at 15:32Z: CI / test 15:32:08Z, release-exercise 15:32:19Z, self-guards 15:32:25Z, action-exercise 15:32:30Z, docs-sync-exercise 15:32:36Z, refs-not-closing 15:32:42Z, labels / labels 15:32:50Z — all seven success, all before the 15:54:52Z merge. The note is spent and the merge was made on green. The double state:* label it also reported is gone; !249 carries one state label now. Recorded here rather than on the merged PR, because this is where the record is read.

Independently at ca7ce6e I ran bash test/run.sh whole: 31/31 test files passed, test/labels-reconcile.test.sh 252/0 and test/forge-backends.test.sh 134/0.

One thing that is deliberately not tidied here

changelog.d/238.md is still on main, unconsumed. It landed with this PR at 15:54, sixty-nine seconds before !250 merged and after that release PR's merge base, so the assembler never took it — which is why CI / self-guards is red at the 0.6.2 tag and why 0.6.2 ships this issue's code without crediting it. That is escalated to the operator as needs-ruling on #231 and its cause is minted as #253. Neither reaches this issue, and this fragment is not debris for a passing builder to clean up.

What the auto-close shape costs, stated once

This is not a #238 defect. Closes #N closed #229, #230, #235 and #236 the same way; #235 and #236 still carry eight unticked criteria each. The claimed label and assignee stay on all of them, and on this one, for the reasons written into the body above — LABELS.md's one-queue-label invariant is scoped to open issues, and on a closed issue the assignment is the clearest record of who built the thing. What actually goes missing on that path is the completion, not the label: nobody re-measures the criteria against the merged head. Triage has now done that here. Whether Closes #N should be allowed at all when an issue carries verifiable criteria is a doctrine question, and it is not settled by a comment on one issue.

✅ **Completion (triage, 2026-08-24T17:11Z) — all eight acceptance criteria re-measured and ticked. `Closes #238` closed this issue at the merge and left nobody holding the completion; this is triage discharging it. Nothing is asked of @codex-bot-andresmgsl, no label is written, and no `attention` is set.** **Label and assignee events paged by hand immediately before this write, not read off the thread**: `bug` + `blocked` + `scope:labels` at the 2026-08-22T23:57:18–19Z mint, `ready` on / `blocked` off 2026-08-24T00:28:42–43Z (triage), `claimed` on 12:35:08Z and self-assign 12:35:09Z (@codex-bot-andresmgsl), then **no queue-label event at all** — the close at 15:54:52Z is by @andres merging !249. Current state: `bug`, `claimed`, `scope:labels`, assigned, **closed**. ## Why this comment was owed at all !249 opened with **`Closes #238`**, so the forge closed this issue itself. That path is not the `Refs #N` one the sweep watches: no `post-merge` transition fires, no transition comment is written, and not one criterion is ticked. This issue sat closed with sixteen empty checkboxes and a lede describing a build in flight. Both are repaired above — the body is rewritten, not annotated. ## The eight criteria, measured at `ca7ce6e` Verified against the tree, not against the PR's own record; where the PR asserted a number I re-ran the thing that produces it. | # | criterion | verdict | evidence | |---|---|---|---| | 1 | Forgejo verb returns exactly `REQUEST_REVIEW` logins | ✅ | `test/forge-backends.test.sh:601` crew!96 three-row payload → `""`; `:627` crew!97 opening payload → the four logins, deduped and sorted from a deliberately unordered five-row fixture | | 2 | GitHub verb returns `.requested_reviewers[].login` | ✅ | `:666` against a stubbed PR object; implementation at `lib/forge-github.sh:105-108` | | 3 | the four superseded questions re-asked at the verb, greppably named | ✅ | all four carry `supersedes` in the assertion name — stale-never-re-requested (`:601`), re-requested-holding-`REQUEST_CHANGES` (`:636`), merged-PR-with-stale-field (`:613`), request-row-only (`:642`) | | 4 | end to end on crew!96, both directions shown | ✅ | `test/labels-reconcile.test.sh:214` old inferred set → `state:bots-reviewing`; `:217` exact empty set → `state:addressing`. The before/after is kept honest by `inferred_requests_before_238` at `:72`, retained **only** as the regression's control | | 5 | `grep -c outstanding_requests actions/labels-reconcile/labels-reconcile.sh` is `0` | ✅ | `0`. `REQUESTED="$(forge_pr_review_requests "$n")"` at `:1047`, still physically after the `REVIEWS_JSON` assignment as spec item 2 requires | | 6 | no assertion deleted or weakened outside the named block; count stated with delta | ✅ | ran it: **252 passed, 0 failed**. The diff of `5be223a` deletes exactly **8** `expect` lines and every one is inside the `outstanding_requests` block spec item 3 names, including the single line item 3a.1 routes. 258 − 8 + 2 = 252, which is the PR body's arithmetic | | 7 | every question #235 added still asked, by name | ✅ | *"three Forgejo request rows keep the opening round with the panel"* survives at `:199` with its `state:bots-reviewing` expectation byte-unchanged and only its setup rewired; *"REQUEST_REVIEW is removed before REVIEWS_JSON reaches the grader"* at `:1718` unchanged with `review_filter_probe`'s override re-pointed at the new verb (`:1711`); the third is superseded at the verb under criterion 3 | | 8 | full suite and sanctioned shellcheck green at the PR head | ✅ | see below | **Criterion 8 deserves its own note, because I filed a board note at 15:22Z saying this half was unproven.** It said all seven checks on head `f0f39076` had been `pending` since 14:42:47Z and had never run, and asked that nobody merge on the strength of a `state:needs-human` with no green run behind it. **The runner drained that queue at 15:32Z**: `CI / test` 15:32:08Z, `release-exercise` 15:32:19Z, `self-guards` 15:32:25Z, `action-exercise` 15:32:30Z, `docs-sync-exercise` 15:32:36Z, `refs-not-closing` 15:32:42Z, `labels / labels` 15:32:50Z — **all seven success, all before the 15:54:52Z merge.** The note is spent and the merge was made on green. The double `state:*` label it also reported is gone; !249 carries one state label now. Recorded here rather than on the merged PR, because this is where the record is read. Independently at `ca7ce6e` I ran `bash test/run.sh` whole: **31/31 test files passed**, `test/labels-reconcile.test.sh` 252/0 and `test/forge-backends.test.sh` 134/0. ## One thing that is deliberately *not* tidied here `changelog.d/238.md` is still on `main`, unconsumed. It landed with this PR at 15:54, sixty-nine seconds before !250 merged and **after** that release PR's merge base, so the assembler never took it — which is why `CI / self-guards` is red at the `0.6.2` tag and why 0.6.2 ships this issue's code without crediting it. That is escalated to the operator as `needs-ruling` on **#231** and its cause is minted as **#253**. Neither reaches this issue, and this fragment is not debris for a passing builder to clean up. ## What the auto-close shape costs, stated once This is not a #238 defect. `Closes #N` closed #229, #230, #235 and #236 the same way; #235 and #236 still carry eight unticked criteria each. The `claimed` label and assignee stay on all of them, and on this one, for the reasons written into the body above — LABELS.md's one-queue-label invariant is scoped to open issues, and on a closed issue the assignment is the clearest record of who built the thing. **What actually goes missing on that path is the completion, not the label**: nobody re-measures the criteria against the merged head. Triage has now done that here. Whether `Closes #N` should be allowed at all when an issue carries verifiable criteria is a doctrine question, and it is not settled by a comment on one issue.
Sign in to join this conversation.
No milestone
No project
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#238
No description provided.