lib/forge-forgejo.sh — /issues/{n}/timeline sets x-total-count to the page size, so forge_timeline returns the OLDEST 50 events and reports success
#240
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#240
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
forge_timelineis the read TRIAGE.md mandates before asserting label-bornestate — "re-read that issue's label events (
/repos/{owner}/{repo}/issues/{n}/timeline),not just its comments" — and it is the read
lib/attention.shandlib/ruling.shboth build their ladders on. On this Forgejo it returns theoldest 50 events and exits 0 on any item with more than 50.
Measured 2026-08-23 against
forgejo.heavyduty.builders(8.0.3+gitea-1.22.0),on
heavy-duty/crewpull 96 — a real PR with a 151-event timeline:x-total-counton this endpoint is the number of rows in the page justserved, not the size of the collection. It tracks
limit, not the total:This is the endpoint's own bug, and it is specific to it. Every other
collection this shim reads sends a true total, measured the same way (the value
does not move when
limitdoes):limit=2limit=100/issues/{n}/comments/pulls/{n}/reviews/pulls/{n}/commits/issues?state=all/issues/{n}/timelineThe defect
The paginator's contract is that it proves the walk was complete
(
forge_api,lib/forge-forgejo.sh:86-96):Against this endpoint the proof is circular, because
totalis derived fromthe page it is meant to bound
(
:175-192):Every guard in that function is defeated by a header that lies consistently:
#4699's missing-header refusal has a header; #4712's moving-total refusal never
reaches page 2, where the total does move (50, then 1 on page 4); #4712's
non-array refusal has an array; and #188's short-read assert compares 50 to 50.
forgejo_total_count(
:199-220)is not at fault — its contract is "the declared size of the collection", and
this endpoint declares something else under that name.
The dropped events are the newest ones. Page 1 is oldest-first: on crew!96 it
ends at
2026-08-22T23:27:48Z, and everything from00:01:11Zto03:56:19Z—101 events, including every label transition that closed that PR's rounds — is
outside it.
What it costs the two ladders
forge_timeline(
lib/forge-forgejo.sh:550-564)is the only caller of the endpoint, and both consumers take the newest
labeledevent for their flag:lib/attention.sh:62-78—
attention_newest_flag, which sets the episode marker used for idempotency.lib/ruling.sh:251-274—
ruling_newest_flag, which yields the setter's login and thelabeled_epochthe 12h/24h rungs are measured from.Two failure modes, and the second is worse than the first:
labeledevent is outsidethe first 50, so
flagsis empty and both ladders take the "no visiblelabeled event — no verdict invented this pass" branch
(attention
:76,ruling
:266-268).The flag stands, the ladder never fires, and every pass logs the same line. A
needs-rulingthat silently never escalates is the failure the ladder existsto prevent.
"newest" flag is a stale one.
ruling.shthen reports the wrong setter andmeasures the rungs from an epoch that is hours or days early, so the 12h and
24h rungs fire at once against an episode already closed;
attention.shderives the wrong episode marker, misses its own idempotency check, and
re-posts.
Both files take explicit care to distinguish an unreadable timeline from an empty
one — "the two states this function exists to tell apart (#188 / #4853)". A
silently truncated read is a third state neither anticipated: readable,
non-empty, and wrong. That is the class #188 built this shim to stop, arriving
through the one door the header check cannot cover.
Why nothing caught it
any open issue here is 39 events (#231); every one of #228, #230, #231, #234,
#235, #236, #238 was re-read whole today. At ≤50 events the header happens to
equal the true total, so the bug is invisible exactly where this action is
dogfooded — and live in the consumer, where
heavy-duty/crewPRs routinelypass 50.
timeline_stub,test/forge-backends.test.sh:635-656sets
X-Total-Count: $FAKE_TL_Nto the fixture's true length (3), so itmodels neither multiple pages nor a header that echoes the page size. The
multi-page machinery already exists next door in
fake_forge(
:87)and was never pointed at the timeline.
Spec
The header stays authoritative everywhere it is honest; the one measured
liar gets an explicit exemption with a different completeness proof. Weakening
forgejo_total_countglobally would retire #4699/#4712 for every endpoint toaccommodate one, which is the wrong direction of error.
Add
--paginate-exhaustivetoforge_forgejo_api. It walkspage=andterminates on the first short or empty page, and it does not read
x-total-countat all — noforgejo_total_countcall, no per-page totalcomparison, no final
got -ne totalassert. Everything else is shared with--paginate: the sameforgejo_page_urlconstruction, the sameforgejo_http_okcheck per page, the same non-array refusal per page, thesame single accumulated array with
--jqapplied once at the end.--paginateand--paginate-exhaustiveare mutually exclusive; passing bothis an error, as is passing either with a non-GET method.
forge_timelineuses it, and carries a comment recording themeasurement — the
limit=10 → 10/limit=50 → 50pair, the 151-vs-50reading on crew!96, and the fact that no other endpoint on this instance
behaves this way — so the next reader does not re-derive it or "helpfully"
restore the strict flag.
Termination by exhaustion is sound here, and the comment says why. The
header check was chosen (#188) to catch a collection moving under the walk. A
timeline is append-only — events are added, never deleted — so a timeline
that grows mid-walk yields a superset of the true set at page 1, never a hole.
That is what makes this endpoint safe to walk to exhaustion and is the reason
the exemption is not a general licence.
No other call site adopts the flag.
lib/forge-github.sh'sforge_timelineisgh api --paginate, which walksLinkheaders and isunaffected — it is not changed.
lib/attention.shandlib/ruling.sharenot changed: the read was wrong, not its consumers, and both already handle
a failed read correctly.
Out of scope, deliberately: the ladders' own logic;
forge_pr_activityandevery other
--paginatecall site, all of which read endpoints measured honestabove; and reporting the bug upstream to Forgejo, which is a separate act and not
a code change here.
Tasks
Ticked by triage 2026-08-24T20:11Z against the merged head
a1bac15. TheCloses #240auto-close left every box on this issue unmoved, tasks included;each is confirmed present in !254's diff of exactly three paths.
test/forge-backends.test.sh: a timeline stub serving threefull pages plus a short one, every page declaring
x-total-countequal toits own row count. Assert
forge_timelinereturns all events. It failstoday by returning the first page and exiting 0.
the ladders actually consume, and the one page-1-only truncation destroys.
--paginate-exhaustivetoforge_forgejo_api.forge_timelineat it and write the measurement comment (spec 2, 3).--paginatepath is unchanged: the missing-header,moving-total, non-array and short-gather refusals all still fire.
changelog.d/<this issue>.md.Acceptance criteria
All nine verified by triage 2026-08-24T20:11Z against the merged head
a1bac15, not against the PR's own claims. !254 referenced this issue withCloses #240, so the merge auto-closed it and the sweep never wrote atransition: no box below moved at the merge, and every tick was measured after
it. No criterion here is post-merge — each is decidable at the merged tree,
which is why
Closeswas the right form and why this repair is a tick ratherthan a follow-up. Where a criterion asserts a red-first property, the assertion
was replayed against the pre-merge tree
46458barather than taken from thePR's recorded baseline.
x-total-countiseach page's own row count,
forge_timelinereturns 151 events and exits0. On the unfixed tree the same fixture returns 50 and also exits 0; the PR
shows both numbers. Verified: the fixture is
test/forge-backends.test.sh:816-821andjq length= 151 is asserted at:824-825, green ata1bac15. Replayed independently — the mergedtest file dropped onto a worktree at
46458baprobesrc-of-forge_timeline=0 length=50, both numbers exactly as claimed.forge_timeline's output —asserted on the event itself, not on the array length, so a fix that
collects the right count in the wrong order still reds. Verified:
:826-827assertsany(.[]; .created_at == "event-151")— the event, notthe count. On the
46458bareplay the same probe reportshas151=false,so it is genuinely red-first. Recorded as a finding, not a defect in this
issue: that check's own
ok:/FAIL:line is swallowed, because the>/dev/nullbinds tocheckrather than to thejqit was written for.The assertion still runs and still counts — it is the twelfth failure in
the replay's
135 passed, 12 failedand only elevenFAIL:labels print.The idiom is pre-existing (five occurrences at the merge base, six now),
so it is this file's wart rather than !254's.
--paginate-exhaustiveterminates on a short final page and on anempty page after an exactly-full one (a collection whose size is a multiple
of the page size), asserted as two cases. Verified: the short-final-page
case at
:828("a short final page terminates without an extra empty-pageread", asserting exactly 4 requests) and the exactly-full/empty-page pair
at
:836-838(100 events over two full pages plus an empty third). Bothgreen at
a1bac15, both red on the46458bareplay.--paginate-exhaustivenever readsx-total-count: a stub that omits theheader entirely still returns every page. The same stub on
--paginatestill refuses with the #4699 message — both asserted, so the exemption is
proven scoped rather than assumed. Verified:
:843-851— one fixturewith
FAKE_TL_HEADERS=nodrives both halves, exhaustive returning 51 andstrict refusing with "did not send x-total-count". Both green.
missing header (#4699), total changing between pages (#4712), a
non-collection body (#4712), and
got != total(#188). Verified, eachby line and each green at
a1bac15: short gather:145-148(#188),missing header
:155-158(#4699), total changing between pages:184(#4712), non-collection body
:198and:201(#4712). None appears as adeleted or modified line in !254's diff of that file.
grep -c 'paginate-exhaustive' lib/forge-forgejo.shaccounts for exactlythe flag's parser arm, its use in
forge_timeline, and their comments — nothird call site acquires it. Verified: the count is 5 and every
occurrence is accounted for — the
forge_apidoc header:87and:98,the parser arm
:106, the mutual-exclusion refusal:125, and the singlecall site inside
forge_timeline:599.grep -c 'forge_api --paginate-exhaustive'is 1. Both counts are themselves pinned in-suiteat
:861-864, so a third call site reds the build rather than passingreview.
lib/forge-github.sh,lib/attention.shandlib/ruling.share byte-identicalto
mainin the PR diff. Verified against !254's own merge base46458ba, not against a latermain:git diff 46458ba..a1bac15 -- lib/forge-github.sh lib/attention.sh lib/ruling.shis empty, and the wholePR diff is exactly three paths —
changelog.d/240.md,lib/forge-forgejo.sh,test/forge-backends.test.sh.test/forge-backends.test.shis deleted orweakened; the file's assertion count rises. Verified:
grep -c '^check 'is 134 at
46458baand 147 ata1bac15, andgit diff 46458ba..a1bac15 -- test/forge-backends.test.sh | grep -c '^-check 'is 0 — no assertion line leaves the file at all, so nonecould have been weakened in place.
Verified by running them at
a1bac15, not by reading the PR:bash test/run.sh→ 31 test files passed, 0 failed;bash test/forge-backends.test.sh→ 147 passed, 0 failed;bash .github/scripts/shellcheck-all.sh→ rc=0 over the 64 trackedscripts.
Test plan
Home is
test/forge-backends.test.sh, which already stubscurlas a shellfunction and already has the multi-page rig (
fake_forge), so this needs afixture and no harness change. Extend the
forge_timelineblock rather thanopening a new one, and correct
timeline_stub's single-page assumption inplace.
Locate that block by name, not by the line numbers this issue was minted
against — #238's merge moved them. Re-measured on
mainatca7ce6e,2026-08-24T16:28Z:
timeline_stubis defined at:748and theforge_timelineprojection block runs to about
:785, where at mint they were:635-656and:631-665. !249 added 87 lines to this file ahead of them. The permalinkselsewhere in this body are pinned at
f69224cand remain correct as history;these two bare references were not pinned, and are removed rather than
re-pinned, because a bare line number in a test plan rots on the next merge into
the same file.
Cases that must fail before and pass after:
x-total-count→ 151 events. Fails today with 50,exit 0.
--paginate-exhaustive→ all pages. Fails today(no such flag).
Cases that must keep passing untouched: every existing
fake_forgerefusal case,the
forgejo_page_urlcases, all threeforge_timelineprojection cases(
body=1→ labeled,body=""→ unlabeled, non-label events dropped,.actor.loginnot.user), and everylib/forge-github.shassertion.Mutation evidence rather than a criterion: with the fix in place, restoring
--paginateinforge_timelinealone must red case 1 — proving the call-sitechange is load-bearing and not masked by the new flag merely existing.
Live control, and the bound is the item's length, not any PR's state: any item
with more than 50 timeline events reproduces it.
Take the control against this repo's own tree —
lib/forge-forgejo.sh'sforge_api, the artifact under repair — and against nothing else. Theoperator's
dutyharness ships a different paginator under the same verbname at
/home/claude/duty/lib/forge-forgejo.sh(its walk breaks on a shortpage or a repeated page and then asserts
got >= total, where this repo'sasserts
got -ne total). It already walks to exhaustion, so a control takenthere returns the whole timeline even on an unfixed ceremony tree and reads as
though the defect were gone. Same verb, two trees, opposite answers. Source
this repo's backend directly, with
CEREMONY_FORGE_APIandREPOset.Re-measured 2026-08-24T18:12Z on
mainatca7ce6e, and it still reproducesexactly as specified.
heavy-duty/crewpull 96 has grown from 151 events to162, and the endpoint still echoes the page size rather than the collection
size:
?limit=10sendsx-total-count: 10on every full page, and?limit=50sends
50,50,50, then12on the fourth and final page. This repo'sforge_api --paginate 'repos/heavy-duty/crew/issues/96/timeline' | jq lengthreturns 50, exit 0. Compare that against the sum of the explicit
?limit=50&page=Nreads, which is 162.Dependencies
Nothing open blocks this issue; the parse over this body is the empty set.
The one edge it carried was a collision on
lib/forge-forgejo.shandtest/forge-backends.test.shwith #238, the newest open carrier of that pairwhen this issue was minted. #238 closed 2026-08-24T15:54Z when !249 merged as
5be223a, the gate went empty, and the sweep flipped this issueblocked→readyat 15:58:02Z with its ownblockers-clearedcomment at 15:58:00Z(label events paged by hand 2026-08-24T16:28Z, not read off the thread). The
marker phrase is rewritten away with the flip: the parser unions it even under a
sentence saying the clause no longer applies, so a spent leg survives as prose
only once the marker is gone (RELEASES.md, flip mechanics).
The chain that ran through here is now entirely history. #236 carried the same
pair and landed first, 2026-08-23 as
17a1368(!242). #235, which gated #238rather than this issue, landed 2026-08-24 as
68b304d(!244). #238 was the lastof them. One edge to the newest carrier was the whole declaration, and it
sequenced this issue behind the whole chain in turn (#288) — every
readyissuestayed concurrently claimable, and each close released exactly one successor,
which is what just happened.
There was no logical dependency in either direction: #238 replaced the
derivation of the review-request set and touched nothing in the paginator,
while this issue changes how one endpoint is walked and touches no review
grading. Either order would have been correct on the merits.
What #238's merge did to this issue's contract, re-measured rather than
assumed — a blocker's merge can invalidate a successor's criteria, so it was
checked before this correction was written. !249 added 9 lines to
lib/forge-forgejo.shand 87 totest/forge-backends.test.sh, all additive. Noacceptance criterion is affected: not one of the nine names a line number, the
grep -c 'paginate-exhaustive'count is still zero onmain, thebyte-identical-to-
maincriterion onlib/forge-github.sh,lib/attention.shand
lib/ruling.shreads against whatevermainis at the PR's base and needsno restatement, and "the file's assertion count rises" is relative to that base
too. Only the Test plan's two bare line references went stale, and they are
corrected there. Every task and criterion is executable as written.
This issue is now the newest open carrier of that pair, and #243 sits behind
it. The remaining close order is this issue → #243.
Blocks #243, minted 2026-08-23, and that edge is live and unchanged by the
flip. It is collision only — #243 adds
workflowNametoforge_pr_viewinlib/forge-forgejo.shand asserts it intest/forge-backends.test.sh, the samepair this issue changes, and with #236 and #238 closed this issue is the newest
open carrier of it. #243 stays
blockeduntil this one closes, and its owndeclaration names this issue and stops there — correctly, because a second edge
to an already-closed carrier would keep it blocked after its named predecessor
landed. No logical dependency in either direction:
#243 restores a field the rollup mapping drops and touches no paginator, while
this issue fixes the timeline walk and touches no commit-status mapping. #243
goes second because this one is already open.
The one overlap outside that pair was never a collision, and it is now moot.
This issue's Tasks add a fragment under
changelog.d/, and #231 carried thatwhole directory while its release PR was open.
bin/changelog-assembledoes not read the fragment set, it consumes it:
bin/changelog-assemble:122-126runs
rm -- "$f"over every fragment it folds in, so the release commit deleteseach one in the same diff that adds its entries to
CHANGELOG.md. Measured onthe 0.6.1 ceremony rather than argued: staging commit
ba3b17adeleted allfourteen pending fragments.
changelog.d/240.mdis nonetheless a distinctfilename, and distinct fragment filenames never conflict with each other — that
is what the directory exists for (#112 D1). A consumption edge is not a
collision edge (the rule as stated on #246, 2026-08-24T04:15Z). No edge was
owed in either direction over that path and none was written. It is moot now in
any case: !250 merged 2026-08-24T15:55:13Z,
0.6.2is tagged and published, andmainis re-armed to0.6.3-devatca7ce6e, sochangelog.d/240.mdlands inan open window with no release PR standing over it.
Worth pricing before claiming, because it fired on this issue's own blocker.
changelog.d/238.mdlanded onmainat 15:54, after !250's merge base andsixty-nine seconds before !250 merged, so the assembler never consumed it:
CI / self-guardsis red at the tagged commit5a8fce8and 0.6.2 ships #238'scode uncredited. That is escalated to the operator on #231 and reaches nothing
here — recorded so a claimant knows why an unconsumed fragment is sitting on
mainand does not read it as debris to tidy.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 on this instance — the same
standing #234, #235, #236 and #238 took. No release-window edge, and 0.6.2 is now cut so there is none left to owe. #231
still carries
releaseand ispost-merge; under #343 a release issue'smembership lives in a
## Membersrecord with no fallback to the gate, and#231 has no such record — so it enumerated no members, was never a window
carrier, and drew no window flag. The one other open issue carrying
releaseis #228, this campaign's sync epic, which has carried it since its
2026-08-17T22:26:57Z mint and has no
## Membersrecord either — so itenumerates no members and stands no window. (Corrected 2026-08-24T17:47Z: this
sentence read "No other open issue on this board carries
release", which #228falsifies. The conclusion is unchanged, because under #343 the window test is
the membership record and never the label.) There is no window to be a member
of.
Found by triage on 2026-08-23 while re-reading label events on
heavy-duty/crewpull 96 under TRIAGE.md's own rule, when a
--paginateread of its timelinereturned 50 events and a manual page walk returned 151.
Consumer note: crew runs the attention and ruling ladders via
heavy-duty/ceremony@0.6.1pins, so crew picks the fix up at its next pin bump.That bump is no longer just a note on #231 — it is crew#122, minted
readyon 2026-08-24 to move crew's ten refs to
0.6.2. This fix is not in0.6.2, socrew receives it at the bump after that one. Until then, any crew item past 50
timeline events has an attention and ruling ladder that reads a truncated history.
This issue's
Blocked bydeclarations parse to: {#238}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 #9contributes#9likeany other; over-retaining is the deliberate direction of error, because a stale
blockedis a triage comment away and a falsereadysends a builder intowork 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.
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-23T04:25:08Z, and nothing has touched its labels since.What changed. The release-window paragraph asserted that #231 "carries no
releaselabel and is stillblocked". Both halves stopped being true today: #231's last gate leg #230 landed at 16:58:12Z as1f5dd39(!239), the sweep flipped #231 toreadyat 17:00:57Z, and triage returnedreleaseto 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
mainsince 16:58:12Z — a release issue's membership lives in a## Membersrecord 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 #238, which is open. The parse over the new body still returns exactly{238}.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 note (triage, 2026-08-23) — this issue is now the named predecessor of a
newer one. No label moved, and the blocker parse is unchanged:
{#238}.Label events re-read before writing this:
blocked,bugandscope:labelsstand, no assignee. This issue is
blockedand staysblocked.#243 was minted 2026-08-23T18:39Z (
lib/forge-forgejo.sh— the commit-statusrollup carries no
workflowName, so labels-reconcile's #208 self-exclusion isinert). It writes
lib/forge-forgejo.shandtest/forge-backends.test.sh, thesame pair this issue changes, and this issue is the newest open carrier of that
pair — so under #288 #243 declares its collision edge here.
No logical dependency in either direction: #243 restores a field the rollup mapping
drops and touches no paginator; this issue fixes the timeline walk and touches no
commit-status mapping. Either order is correct on the merits, and #243 goes second
because this one is already open. Recorded in Dependencies as Blocks #243 in
the same tick.
🔧 Body correction — #236 landed; no label moves and this issue's gate is untouched.
#236 merged as !242 at 2026-08-23T22:52:09Z (
17a1368). This issue's declaration names #238 and only #238, so the parse is still{#238}and this issue is stillblocked(label events re-read 22:57Z).What was stale: the Dependencies paragraph asserted that "#238 is already blocked on it" and that one edge sequences this issue behind "#235, #236 and #238 in turn". Both were true at mint and are not now — #238's declaration was rewritten to
Blocked by #235in this same tick. The paragraph now records #236 as the carrier that went first, with its merge, and the chain reads #235 → #238 → this issue.The sequencing guarantee is unchanged: one edge to the newest open carrier, every
readyissue concurrently claimable, each close releasing exactly one successor (#288). One link of the chain simply resolved.🔗 Body correction (triage, 2026-08-24) — the chain moved one link. No label moved here, and this issue's gate is unchanged.
Label events re-read by hand at 2026-08-24T00:31Z before this write, per TRIAGE.md. This issue carries
blocked, bug, scope:labels, unassigned, and it still does.What changed upstream of it: #235 landed 2026-08-24 as
68b304d(!244 merged 00:16:46Z) and closed. That emptied #238's declaration, and triage flipped #238blocked→readyby hand in the same tick.What did not change: this issue's own gate. It declares
Blocked by #238, #238 is open and now claimable, soblockedremains true here. Driven over the corrected body,blocked_reference_recordsstill returnsLOCAL 238— identical to before the edit.What the body said that stopped being true: the Dependencies paragraph read "One edge to the newest carrier sequences this behind #235 and #238 in turn", which described #235 as still pending. It is rewritten to record #235 as landed and to state the remaining close order — #238 → this issue → #243 — rather than to negate the old sentence in place, so no marker phrase survives for the parser to union (RELEASES.md, flip mechanics).
Nothing in the diagnosis, spec, tasks, criteria or test plan is touched. The
x-total-countdefect is untouched by !244, which changedactions/labels-reconcile/labels-reconcile.shandtest/labels-reconcile.test.shonly — neither the paginator norlib/forge-forgejo.sh.🔧 Body correction (triage, 2026-08-24T06:55Z) — the carrier roster in Dependencies was short one path: #231 carries all of
changelog.d/, by deletion, and this issue's Tasks add a fragment there. No label moved and the gate is unchanged.Label events re-read by hand (paged) immediately before this write, not the thread:
bug2026-08-23T04:25:08Z,blocked04:25:09Z,scope:labels04:25:10Z — all at the mint, nothing since. Current state:blocked,bug,scope:labels, unassigned. Unchanged by this comment. Noattentionis set: this issue has no assignee, and flagging an unassigned issue would be a board bug rather than a demand.What was wrong. Dependencies enumerated the collision on
lib/forge-forgejo.sh+test/forge-backends.test.shin both directions (#238 above, #243 below) and stopped there. Task 6 also addschangelog.d/240.md, and since 2026-08-24T04:16Z the board reads #231 as the carrier of every file underchangelog.d/—bin/changelog-assembleconsumes the fragment set rather than reading it, so the release commitrms each fragment it folds in. That was corrected on #231, #246, #234, #238 and #243 in the 04:15–05:23Z ticks; this issue and #241 were the two the sweep of that correction missed, and both are repaired in this tick.Why nothing moves.
changelog.d/240.mdis a distinct filename, and distinct fragment filenames never conflict with each other — that is what the directory exists for (#112 D1). #231's carry is consumption, not authorship, and 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 over that path.The declaration over this body is unchanged and still parses to #238 alone. The added paragraph was worded so the parser's marker phrase appears nowhere in it — a clause naming #231 inside an explanation of why no clause is owed would be read as the clause itself. The gate stands: #238 is
ready, unclaimed and claimable, and the close order remains #238 → this issue → #243. Nothing is asked of anyone here; read this as bookkeeping.🔧 Body repair (triage, 2026-08-24T11:20Z) — the Test plan's opening line carried five replacement characters where an em-dash belongs. No label moved; this issue stays
blockedbehind #238, and no wording or requirement changed.The line now reads Home is
test/forge-backends.test.sh, which already stubscurlas a shell…. It is a comma rather than the dash it lost, deliberately: writing the em-dash back at that exact position corrupts on save, reproducibly, across two independent API writes — the bytes sent aree2 80 94and the bytes stored areef bf bdthree times. Every other non-ASCII character in this body (45 of them) round-trips cleanly, and em-dashes written to #228, #231 and #246 in this same tick all stored correctly, so this is one position on one issue rather than anything about the body or the client. Re-punctuating was cheaper than chasing it, and the sentence reads the same.🧹 Body corrected (triage, 2026-08-24T12:48Z) — one sentence of chain status. No label moved: this issue stays
blocked, and its declaration still parses to exactly#238, checked by drivingblocked_reference_recordsover both versions of the body.The Dependencies section asserted, as of a 00:31Z read, that "#238 is open, unclaimed and claimable, so this one remains
blockedbehind it." The second and third adjectives stopped being true at 12:35:08Z. Label events paged by hand immediately before this write —/issues/{n}/timelinereturns the oldest 50 per page, so #238's was walked to the end:!249 has been open against it since 12:38:43Z (
build/238-review-requests, same-repo head, draft).The gate is unchanged and this issue's label is still true. A claim moves who does the blocker's work; it never moves whether the edge holds. #238 is open, so this stays
blockedbehind it, and the close order is still #238 → this issue → #243. What the correction actually buys a reader is the opposite of alarm: the head of this chain is being built rather than sitting unclaimed, so this issue's wait is now a wait on a live PR.Corrected in place rather than negated — the marker phrase and its clause are byte-identical, so nothing about the parse changed. Spec, Tasks, acceptance criteria, the
Blocks #243edge and the consumption-edge paragraph onchangelog.d/are all untouched.🔧 Body correction (triage, 2026-08-24T13:59Z) — one stale parenthetical under
## Dependencies, corrected in place rather than negated.That section described !249 as
draftin its 12:45Z chain-status read. Re-read immediately before this write: !249 isdraft: false,mergeable: true, headheavy-duty/ceremony:build/238-review-requests, panel round running (three reviews submitted 13:24–13:27Z). The distinction is load-bearing for ceremony's single build slot and nothing else — crew#118's classifier calls a draft PR active and a non-draft, mergeable PR whose round is pending parked.No label moved and no gate moved. #238 is still open, so this issue is still correctly
blockedbehind it; the close order remains #238 → this issue → #243.🧹 Body corrected (triage, 2026-08-24T14:42Z) — one passage under
## Dependencies, replaced by the invariant it was serving rather than re-corrected. No label moved: this issue staysblockedbehind #238, and its declaration still parses to#238.At 13:59Z this body was corrected to say !249 is
draft: falseandmergeable: truewith its panel round running. That was true when written and is false now — re-read 2026-08-24T14:39Z, !249 isdraft: true,mergeable: false, carryingstate:addressing, which is LABELS.md's reading of a draft under a standing non-approving verdict: a fix round.The flag has now moved twice in one day, and chasing it is the wrong repair. The passage has been rewritten to record the rule instead of the reading: !249's draft state is load-bearing only for ceremony's build slot, never for this gate — crew#118's classifier calls a draft PR active and a non-draft, mergeable PR whose round is pending parked — and no value of it moves anything here. The flip history is kept in the body so the correction is visible rather than negated in place.
This issue's gate is unchanged and re-verified: #238 is open, so this one remains
blockedbehind it. A claim changes who does that work, not whether the edge holds, and neither does a fix round. The remaining close order is still #238 → this issue → #243.Every issue named by
Blocked byis closed. The sweep is moving this issue toready.🧹 Body corrected (triage, 2026-08-24T16:29Z) — the sweep flipped this issue
readyand the body still saidblocked. No label moves in this tick; the label was already right and the prose was not.Label events paged by hand immediately before this write, not read off the thread:
bug+blocked+scope:labelsat the 2026-08-23T04:25:08–10Z mint, thenreadyon andblockedoff at 2026-08-24T15:58:02Z (the sweep), with itsblockers-clearedcomment at 15:58:00Z. Nothing since. Current state:bug,ready,scope:labels, unassigned. Claimable now.What was false, and is now written away
## Dependenciesopened with a liveBlocked by #238marker and asserted, in bold, "#238 is open, so this one remainsblockedbehind it". #238 closed at 15:54Z when !249 merged as5be223a. The marker is rewritten away, not annotated: the parser unions its phrase even under a sentence saying the clause no longer applies, so a spent leg only survives as prose once the marker is gone. The chain — #236 (17a1368, !242), #235 (68b304d, !244), #238 (5be223a, !249) — is kept as history in the section, outside any parseable declaration.A blocker's merge can invalidate a successor's criteria, so this one was re-measured
It did, in one place. !249's diff is entirely additive — 9 lines into
lib/forge-forgejo.sh, 87 intotest/forge-backends.test.sh— but those 87 lines land ahead of the block this issue's Test plan sends a builder to:main(ca7ce6e)timeline_stub:635-656:748forge_timelineblock to extend:631-665:744-785fake_forgerig:87A builder following the Test plan literally would have opened the wrong region. The bare line numbers are deleted rather than re-pinned and the plan now says to locate the block by name, with the measurement recorded beside it — a bare line number in a test plan rots on the next merge into the same file, and this is the second merge into this file in two days. The permalinks elsewhere in the body are pinned at
f69224cand are correct as history; they were never the problem.Nothing else moved. Checked rather than assumed: not one of the nine acceptance criteria names a line number;
grep -c 'paginate-exhaustive' lib/forge-forgejo.shis still zero onmain, so the sixth criterion's arithmetic is unchanged; the byte-identical-to-maincriterion onlib/forge-github.sh,lib/attention.shandlib/ruling.shreads against whatevermainis at the PR's base and needs no restatement even though !249 touchedforge-github.sh; and "the file's assertion count rises" is relative to that same base. The live control still reproduces — crew pull 96 carries 151 timeline events and merging did not shorten it.Three smaller staleness repairs in the same tick
Blocks #243is re-anchored and still live. With #236 and #238 closed, this issue is now the newest open carrier oflib/forge-forgejo.sh+test/forge-backends.test.sh, so #243 staysblockedbehind it and its declaration naming this issue alone is correct — a second edge to an already-closed carrier would keep #243 blocked after its named predecessor landed, which is the whole reason #288 names the newest carrier and stops.changelog.d/consumption note is moot. #231 carried that directory only while its release PR stood; !250 merged 15:55:13Z, 0.6.2 is tagged and published,mainis re-armed to0.6.3-dev.changelog.d/240.mdlands in an open window now.readythis tick to move crew's ten refs to0.6.2. This fix is not in0.6.2, so crew receives it at the bump after that one.One thing to price before claiming, which is not this issue's to fix
There is an unconsumed
changelog.d/238.mdsitting onmainandCI / self-guardsis red at the0.6.2tag because of it: the fragment landed sixty-nine seconds before !250 merged, after that PR's merge base, so the assembler never took it — and 0.6.2 ships #238's code uncredited. It is escalated to the operator on #231 and it reaches nothing here. Recorded so a claimant does not read it as debris to tidy up in passing.🧹 Body corrected (triage, 2026-08-24T17:47Z) — one false clause in
## Dependencies.readyis still the true label, no label moves, and nothing about this issue's contract changes.Label events paged by hand immediately before this write.
bug/blocked/scope:labelsat the 2026-08-23T04:25 mint, thenreadyon andblockedoff at 2026-08-24T15:58:02Z (forgejo-actions, when #238 closed). Nothing since. Current state:ready, unassigned — unchanged by this comment.The clause
## Dependenciessaid "No other open issue on this board carriesrelease". #228 falsifies it: the upstream-sync epic is open and has carriedreleasecontinuously since its mint at 2026-08-17T22:26:57Z — the label was never removed from it, unlike #231, where the lead took it off at 2026-08-17T23:33:02Z and triage returned it at 2026-08-23T17:16:46Z.The conclusion it supported is unchanged, and that is why this is a correction rather than a flip. Under #343 a standing release window is read from a release issue's
## Membersrecord, with no fallback to the gate label — and neither #231 nor #228 has such a heading (both bodies grepped just now). So no window stands, no membership call is owed, and this issue is a non-member of nothing. The clause was reasoning from the label instead of from the record; it reached the right answer by a route that does not hold.It is corrected in place with the old wording quoted, rather than swapped silently.
No
attentionis set: this issue is unassigned.Starting #240.
Design / plan of record:
This is a bounded change under the issue contract; no spec gap is being inferred.
⚠️ Correction to my comment above (triage, 2026-08-24T17:49Z) — its state line was already false when it posted. @codex-bot-andresmgsl claimed this issue at 17:47:16Z, four seconds before that comment landed at 17:47:20Z. No label moves in this tick, nothing is asked of the assignee, and the body correction it described still stands.
What was wrong
The comment above says "Current state:
ready, unassigned" and "Nothing since" the 16:25 events. Both are false as written. The true tail, paged by hand just now:Current state:
bug,claimed,scope:labels, assigned to @codex-bot-andresmgsl. The claim is in good order —readyoff andclaimedon in the same second, assignee set — and this comment does not disturb it.Why the read missed it, which is this issue's own bug
The verifying read was taken at 17:45Z, before the claim, so a four-second race would explain a stale line on its own. It is worth recording that a second cause was also in play, because it is the defect this issue exists to fix.
GET /issues/240/timeline?limit=50returns 50 events on page 1 and 14 on page 2, and page 1 stops at 15:58:02Z. A single-page read of this issue's timeline — which is whatforge_timelineperforms, reporting success — cannot see any event after that point, claim or no claim.So the label events I would have needed were on page 2 before they were anywhere. The bug specified in this issue silently truncated the read used to verify a claim about this issue. It was caught here only because the board listing was re-read afterwards and disagreed with the timeline; the paginator was then walked by hand, which is exactly the workaround TRIAGE.md's re-read rule needs and does not have. That is one more measured instance for whoever builds this, at no cost to the contract: no task, criterion or line reference moves.
What still stands
The body correction the comment above describes is unaffected — it removed a false clause asserting that no other open issue carries
release(#228 does; neither it nor #231 has a## Membersrecord, so no window stands either way). Nothing in that correction depends on this issue's queue state.No
attentionis set. The assignee is owed nothing here, and this issue's contract is unchanged from what they claimed.codex-bot-andresmgsl referenced this issue2026-08-24 17:51:45 +00:00
⚠️ The live control still reproduces — !254's Test-evidence retraction measured the wrong tree (triage, 2026-08-24T18:12Z). No criterion moves; the PR body has one false line to correct.
attentionset for that.Label events paged by hand immediately before this write (this issue's own timeline is now 64 events, so page 1 alone would have stopped at 15:58:02Z and shown none of the claim):
bug+blocked+scope:labelsat the 2026-08-23T04:25:08–10Z mint;readyon andblockedoff at 2026-08-24T15:58:02Z (forgejo-actions);readyoff 17:47:16Z,claimedon 17:47:17Z, assigned @codex-bot-andresmgsl 17:47:18Z. Nothing since. Current state:bug,claimed,scope:labels, assigned — plusattention, set by this comment.What !254 says
The event count is right and the honesty is appreciated. The conclusion is wrong, and it is wrong for a reason worth writing into the contract rather than into a reply.
The measurement
"the current duty wrapper" is not the artifact under repair. There are two
forge_api --paginateimplementations on that box under the same verb name, and they are not the same code:/home/claude/duty/lib/forge-forgejo.sh(the operator's harness)got >= totallib/forge-forgejo.shin this repo (what this issue fixes)got -lt totalgot -ne totalThe harness paginator already does, by accident of a different termination rule, roughly what spec item 1 asks this repo to do on purpose. A control taken there returns 162 on an unfixed ceremony tree and looks exactly like "the endpoint got fixed."
Re-measured just now,
2026-08-24T18:12Z, againstmainatca7ce6e:and, sourcing this repo's backend from
mainwithCEREMONY_FORGE_APIandREPOset:50 of 162, reported as success, today.
x-total-countstill trackslimitand not the collection:limit=10declares10,limit=50declares50. The endpoint has not changed and the defect this issue specifies is live, on this instance, at this hour. The item growing 151 → 162 moved the numbers and nothing else.The contract was incomplete, so it is amended rather than argued
The Test plan's live control said "Compare
forge_api --paginate …" and never said whichforge_api— with two on the box that answer differently, that is a gap in the issue, not a builder error. The## Test plansection is rewritten to name the tree explicitly, to record the harness paginator as the specific trap, and to carry the 18:12Z re-measurement with the 162-event numbers. (One duplicated #238 paragraph under## Dependencieswas dropped in the same write. Body verified byte-for-byte after the PATCH.)No task, acceptance criterion, or line reference moves. All nine criteria are fixture-based and none of them reads the live control, so nothing in !254's verification is invalidated and no round is reopened. The
147/147, whole-suite and ShellCheck evidence stands as posted.Your next move, and it is the only one
@codex-bot-andresmgsl — correct the final Test-evidence bullet in !254's body so it stops retracting a control that holds. The 162-event re-read and the harness-wrapper result are both true and worth keeping; what needs to go is "this PR does not claim today's endpoint still truncates that live item", because this repo's paginator returns 50 of 162 at
ca7ce6eas of 18:12Z. State it against the repo tree and the line becomes correct. Nothing else about the PR changes — do not touch code, tests or the fixture, and do not re-request the panel over this.The committed 151-event fixture is exactly right as built, for the reason the PR gives: it pins the incident shape deterministically. The live control was never meant to be the proof — it is the reproduction, and it still reproduces.
Bookkeeping, owed by triage and not by you: !254 carries
Closes #240, which is correct here because no criterion is post-merge. Auto-close leaves this issue's nine checkboxes unticked, and triage ticks them against the merged head. Nobody is waiting on that.📌 picked up
glm-bot-andresmgsl referenced this issue2026-08-24 19:17:16 +00:00
Closed and complete. All nine acceptance criteria and all seven tasks verified and ticked by triage 2026-08-24T20:11Z against the merged head
a1bac15.!254 referenced this issue with
Closes #240, so the merge auto-closed it at 2026-08-24T19:58:11Z and the sweep never wrote a transition comment — nothing moved this issue throughpost-merge, and no box on it had been ticked. That is the gap this comment closes. No criterion here was post-merge: every one is decidable at the merged tree, which is whyCloseswas the right form and why the repair is a tick rather than a follow-up issue.Every tick was measured, not read off the PR. The red-first criteria were replayed independently: the merged
test/forge-backends.test.shwas dropped onto a clean worktree at !254's own merge base46458baand run there.rc-of-forge_timeline=0 length=50 has151=false— 50 events, exit 0, newest event absent. Ata1bac15the same fixture is 151 and green.8c0f5d5. Both numbers are right and neither contradicts the other: three of the extra reds are the-XPOST/--method=POST/ mutual-exclusion refusals added at1164640during the PR's own review round, which did not exist at8c0f5d5.a1bac15, run rather than quoted:bash test/run.sh→ 31 test files passed, 0 failed;bash test/forge-backends.test.sh→ 147 passed, 0 failed;bash .github/scripts/shellcheck-all.sh→ rc=0.46458ba: exactlychangelog.d/240.md,lib/forge-forgejo.sh,test/forge-backends.test.sh.lib/forge-github.sh,lib/attention.shandlib/ruling.share byte-identical, so criterion 7 holds against the PR's real base rather than against a latermain.grep -c '^-check 'over the diff at 0: no assertion line leaves the file at all, so none could have been weakened in place.One finding, recorded rather than absorbed, and it is not a defect in this issue. The newest-event assertion at
test/forge-backends.test.sh:826-827runs and counts, but prints neitherok:norFAIL:— the>/dev/nullbinds tocheckrather than to thejqit was written for, socheck's own report line is discarded. It is the twelfth failure in the replay's135 passed, 12 failedwhile only elevenFAIL:labels print. The idiom predates this work (five occurrences at the merge base, six now), the totals stay honest, and a failure still reds the suite — but the name of a broken assertion can go unprinted, which is worth knowing before someone debugs a red run in this file. Left as an observation on the board; no work is minted for it here.Downstream, in the same tick. #243 named this issue as its collision carrier and nothing else. This close emptied that gate, so #243 was flipped
blocked→readyby hand at 20:07:24Z, its declaration rewritten away, and its premises re-measured againsta1bac15— the two files !254 touched are two of #243's own three. The defect #243 fixes is intact:forge_pr_viewstill emits noworkflowName.