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:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#27
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
Five issues were open on stoke when this epic was written (#1, #23, #24, #25,
#26); #26 merged as !29 and closed (2026-08-19), #24 merged as !35 and closed (2026-08-21), and #25 merged as !37 and closed (2026-08-30), leaving two. "leaving two" is false as of 2026-08-31 and is kept as written, because the way it
rotted is the finding (triage, 2026-09-03). This tally was maintained three times — once
per closure, #26 then #24 then #25 — and then abandoned. Measured from the forge this tick:
#1 closed 2026-08-31T16:05:05Z (merged as !38,
fb5cb474,16:05:04Z) and #23 closed 2026-08-31T18:42:40Z (merged as !41,
c34a8b04, same second). All five are closed; zero of them remain open. Read at face valuetoday the clause sends a reader hunting for two open children of this epic that do not exist, and
it contradicts this body's own Task list below — every one of the five rows is
[x], and the list'sown summary line says "Every issue this list references except #54 is closed". The epic's only
open child is #54, created 2026-09-02T09:14:16Z, which is not one of the five.
Why five sweeps walked past it: it is not an imperative and it does not carry any of the nouns
the standing detectors hunt — it is a running tally over board state, and a tally that was updated
three times reads as maintained, which is exactly what makes the fourth miss invisible. All five were found
the same way — by driving a real workflow through the CLI and hitting the
wall, not by reading the command list. That gives them a common shape worth
naming: each one is a point where a task had to leave the CLI and hand-roll a
curlwith the token pasted in, which is the exact credential handling theCLI exists to remove.
This epic evaluates them against each other, assigns a priority, and records
two findings that change what two of them are actually asking for.
Measured against the live instance on 2026-08-05, with
stoke1.2.0 installedfrom the Forgejo Debian registry and
mainat 1.3.0.Spec
Priority order
issue createcannot set labelsneeds-triagein the gap, producing a queue-label conflict that needs two more API calls to clean up. Measured on ceremony#197–#200. The fleet is minting issues now.repo createcannot target an org — landed-o, --owner;createwas the odd one out. Shipped in !35, merged 2026-08-21T23:18:38Z (c09943e); #24 closed the same minute.release createcannot upload assets — landed0.1.0/0.1.1are a 29 MB and a 50 MB installer each. Shipped in !37, merged 2026-08-30T13:12:37Z by @andres (033a40c); #25 closed the same minute on the PR'sCloses #25. The id wrinkle is answered directly:release createnow prints the numeric release id, andrelease uploadresolves a tag to it.[trusted=yes]verification gap. Shipped in !38, merged 2026-08-31T16:05:04Z by @andres (fb5cb474, head2efc76f2, all three panelists APPROVED at that head); @andres closed #1 by hand one second later rather than letting it derive topost-merge, since !38 saysRefs #1. The 2026-08-30T12:32Z park was answered at 16:28Z the same day with option B, never downgrade automatically — and the premise that ruling rested on (a private org) has since inverted: the registry reads anonymously, so the merged README documents the anonymous install as the default.repo syncFinding 1 — #1 is substantially already done, and its remainder is a security gap
#1 reads as "build apt distribution". Measured today, that distribution exists
and works:
dpkg -l stoke→ii stoke 1.2.0 all, installed from the registry;(
/api/v1/packages/heavy-duty→debian stoke 1.3.0);(
/api/packages/heavy-duty/debian/repository.key→ HTTP 200, 1631 bytes);/etc/apt/keyrings/forgejo-heavy-duty.asc;v1.3.0exists on the repo.Four of #1's six acceptance criteria are therefore already met. What is not
met is the one that matters most, and it is not visible from the issue:
[trusted=yes]disables signature verification. The keyring is installedand unused; the
signed-by=form #1's own acceptance criteria specify is notwhat is deployed. This is precisely the failure open PR #21 describes — the
install-apt.sh[trusted=yes]fallback being "too broad" and firing onfailures that are not signature failures, permanently disabling verification.
Correction, triage 2026-09-03:
#21has not been an open PR since 2026-08-30T08:59:29Z. Itclosed unmerged, and this body already records that twice — L433 ("#21 was a PR, not an
issue, and was out of scope here") and the #1 row in the task list ("!21 had closed unmerged and
its fallback narrowing was folded into #1 rather than re-minted"), ~370 lines below this sentence.
The sentence above is kept verbatim: the failure it describes is real, it is what #1 was
rescoped onto, and !21's text is still the clearest statement of it. Only the word "open" is
wrong. Read it as "the failure PR !21 described".
Also observed:
apt-cache policy stokelists only/var/lib/dpkg/statusanda candidate of 1.2.0, so this box cannot currently see the 1.3.0 the registry
is serving. Whether that is a stale
apt-get updateor a repo the client isfailing to read is the first thing to determine — a package registry a client
silently cannot read is the same "reports success, read nothing" shape
ceremony#188 was written about.
So #1 should be rewritten as: close the
[trusted=yes]gap, confirmsigned-by=verification end to end on a clean container, explain the versionskew, and tick the criteria already met. That is a much smaller and much more
valuable piece of work than the issue currently describes. PR !21 carried part
of it — the fallback narrowing — but was closed unmerged on 2026-08-30 with its
head unmoved since 2026-07-26, so that work now lives in #1's own Tasks. The
five findings !21 carried that are not in #1's scope are inventoried in #1's
2026-08-30 comment and currently have no issue.
Finding 2 — #23's design does not cover the case that motivated it
#23 proposes
repo syncas fast-forward only, "refusing a non-fast-forwardrather than forcing it", on the reasoning that "the common case is a mirror
that has not diverged".
That assumption does not hold for the repo #23 names as its concrete case.
heavy-duty/ceremonyon this forge has diverged on purpose: 30 commits ofForgejo-compatibility work that upstream does not have and must never lose
(ceremony#188/#191/#194/#195). Upstream is 157 commits ahead. Their
merge-baseis real —84bb1a4— so the sync is a genuine three-way mergeproducing 18 conflict hunks, five of which need a human or agent decision. It
is tracked as ceremony#197/#198.
A fast-forward-only
repo syncwould refuse that repo forever, and a--forcewould destroy the delta. Neither is the operation actually needed.So #23 needs its spec revised before it is buildable. The honest split:
repo synchandles the undiverged mirror — fast-forward or refuse, asdesigned. Legitimate and useful; just say plainly that it does not serve a
tree that carries local commits.
work is ceremony#200, and it belongs there, not here.
mirror: trueon migration)is the right answer for read-only mirrors and should be decided explicitly
rather than left as an aside — it may remove most of the need for the verb.
Finding 3 — #23, #24 and #25 share files but are three deliverables, and owe no collision edges
Recorded 2026-08-20, after a triage tick briefly serialized them and reverted it the same
tick. Kept here so the question is not re-opened from scratch.
The shared surface is real. All three edit
src/cli.js— a single 1653-line file inwhich every command group is chained (
repoat L326,releaseat L985) — plussrc/api.jsand a
README.mdsection. #23 in particular adds a subcommand to the samerepogroup #24modifies.
It does not owe collision edges. TRIAGE.md's collision rule
(#288) is scoped to one deliverable carried twice, not to any two issues that touch one
large file, and its "disjoint regions do not waive it" clause refers to regions of that
shared deliverable. The sweep that enforces it reads the title's em-dash prefix,
normalized and case-folded (
deliverable_key, issueflow-reconcile 0.6.1 L433):repo syncrepo create --ownerrelease create --asset / release uploadThree distinct keys, so the collision flag does not fire on this trio — correctly. Ordinary
same-file contention between distinct deliverables is a rebase, which is the builder's
normal cost and is not a board gate; turning it into one would have made two smaller,
higher-priority issues unclaimable behind the largest.
Correction, 2026-08-30 — and its own condition has since fired, so read both halves.
This paragraph used to add "and already carries its own PR label (
blocker:conflict)".At the pin this repo ran until 19:28Z today it did not:
blocker:conflictis derived fromForgejo's
mergeableboolean, which isfalsefor every draft PR whether or not itconflicts, so the label fired on all of them and could not be told apart from a real
rebase — #36 defect 6,
which was live on !38 and re-derived twice over hand-removals at 18:08:25Z and 19:17:57Z.
That correction said "do not read
blocker:conflictas evidence of same-file contentionuntil #36's re-pin lands". The re-pin landed at 2026-08-30T19:28Z (
4a62f7e,92ba146— the workflow pins only; #36 records what else the bump left behind), and twominutes later sweep run 453 logged
labels: #38: state -> state:building (cleared blocker:conflict)on a still-draft PR, where it has stayed through the 20:00Z cron. Sothe caveat is spent: at
0.6.3blocker:conflictdoes mean a real merge conflict again,and the clause this paragraph dropped would now be true. It is left dropped because the
finding's conclusion never rested on it — noted here rather than restored so that the
reason the caveat existed stays legible.
All three were therefore left
readyand concurrently claimable when this was recorded on 2026-08-20, as this epic intended (#24, #25 and #1 have since merged and closed — !35 on 2026-08-21, !37 on 2026-08-30, !38 on 2026-08-31). Theshared-file fact is worth carrying into any future wave plan — RELEASES.md has release-init
graph "same-file clusters" when it orders waves — but stoke runs no version epic and no
release window stands, so no wave plan is in force.
Finding 4 — all three CLI gaps are closed, and one workflow in this repo still hand-rolls the curl anyway
SUPERSEDED IN ITS OPENING BY !55 — original text kept verbatim below, nothing deleted (triage, 2026-09-03). This heading and the first four claims under it are the 2026-08-30 measurement, and they are false in the present tense as of !55, merged as
d6a21c9dat 2026-09-02T11:22:58Z. Re-measured atmain2230ca25:.forgejo/workflows/release.ymlis 43 lines and contains 0 occurrences ofcurl— it was 56 lines with 3 at967efa02, where L47-55 was exactly the hand-rolled block this finding names — and its Create release and attach .deb step is now one line delegating toscripts/publish-release.sh. Read as historical, not as current state: (1) the heading's "one workflow in this repo still hand-rolls the curl anyway"; (2) "it decides whether the second acceptance criterion below can be ticked. It cannot"; (3) ".forgejo/workflows/release.ymlL47-55 still does all of it by hand"; and (4) "So the criterion stays unticked". That criterion is ticked —## Acceptance criteriabullet 2, ticked on !55's merge — and its own annotation ends "See finding 4.", so a reader following a live pointer out of a ticked box lands on four superseded present-tense claims. The finding's reasoning is untouched and is why #54 exists; only its tense is spent. Nothing below this block is edited: the later paragraphs go on describing the threecurls in the present on purpose, because they record what was measured about them while they existed.Recorded 2026-08-30T13:20Z, minutes after !37 merged, because it decides whether the
second acceptance criterion below can be ticked. It cannot, and the reason is worth
writing down rather than leaving as an unticked box.
The capability is complete. Verified on
mainat033a40c, in the tree rather thanfrom the PR descriptions:
mainsrc/cli.jsL458-o, --owneronrepo createsrc/cli.jsL721--label <name...>onissue createsrc/cli.jsL1104-1105--asset/--asset-name, L1147release upload,src/api.jsL277uploadReleaseAsset(Every
src/cli.jsnumber in this table is a033a40cnumber and all four are spent atmain;the column header "surface on
main" is what makes them read otherwise. Table kept verbatim —it was exact at the commit the sentence above it names. Triage, 2026-09-03.) !41
(#23,
repo sync, merged 2026-08-31 asc34a8b04) inserted a newrepo synccommand block atsrc/cli.jsL455-498, above everything this table cites, socli.jswent 1742 → 1786lines and every row below shifted by exactly +44:
--label <name...>L721 → L765,--assetL1104 → L1148,--asset-nameL1105 → L1149,.command('upload')L1147 →L1191.
src/api.jsL277uploadReleaseAssetis untouched (393 lines at both refs) and isstill exact. Row 1 is the one to be careful with, and it is why "resolve the citation" is not
enough on its own: at
033a40c,cli.jsL458 isrepo create's.option('-o, --owner <owner>', 'repository owner (authenticated user or organization)'); at2230ca25that same line number is.requiredOption('-o, --owner <owner>', 'repository owner')— which belongs to
repo sync. The stale citation still lands on an-o, --ownerline, so itconfirms itself to a reader who checks the flag and not the enclosing command.
repo create'sown
-o, --owneris at L502 atmain. Nothing in the table's claim changed — all threecapabilities still ship and the epic's conclusion is unaffected; only the numbers moved.
The fact on the ground is not.
.forgejo/workflows/release.ymlL47-55 still does allof it by hand —
curlto fetch or create the release, thencurl -X POST -F "attachment=@$DEB", each withsecrets.RELEASE_TOKENin anAuthorization: tokenheader. That is the exact shape this epic's context paragraph namesas the thing the CLI exists to remove, sitting in this repository, in the workflow that
publishes stoke itself.
So the criterion stays unticked, and it is a capability criterion being held to a
deployment fact. Both readings are defensible and picking one silently would be the
error: "needs" is satisfied — no workflow has to hand-roll anything now — while
"does" is not.
Not minted, and the reason was the spec, not the size — but that reason has since been
measured and it does not hold. Read both halves. This paragraph used to say: "Converting
that workflow raises a question this epic cannot answer from the repo:
release.ymlwouldhave to run the CLI it is in the act of publishing, and how it authenticates there is
undecided — the raw calls pass a token in a header, while
stokereads its own config. Thecheckout makes
node src/cli.jsavailable with no install, so the shape is probably small,but 'probably' is exactly what an issue may not carry." (Said as "#32's release window"
until 2026-08-31; reworded because the 2026-08-30 re-vendor made release window a
machine-parsed term — see the paragraph above, which is the one that still holds: no window
stands here.)
Triage settled it 2026-09-01 at
9586d2c, by running the CLI rather than reading it. Theauthentication question is answerable from the repo, and the answer needs no source change:
auth logintakes-t, --token <token>and--token-file <path>, and either onesuppresses both interactive prompts —
src/cli.jsguards each on!token(
if (!username && !token),if (!password && !token)), so neither is read.--config <path>flag (src/cli.jsL24-26, apreActionhook that setsSTOKE_CONFIG_FILE) puts that config anywhere, so CI need not write into$HOME.9586d2c:node src/cli.js --config <tmp> auth login --url … --token …→Authenticated as claude-bot-andresmgsl using provided token, exit 0; thennode src/cli.js --config <tmp> release list -o heavy-duty -r stoke→v1.3.0, exit 0,no prompt. The stored config is
{url, login, username, email, token, tokenId: null}.--token-fileover--token: the latter puts the secret inargv.The CLI covers the whole block, and the
RELEASE_IDplumbing disappears rather than beingported.
release view --tag --jsonreplaces the L47 lookup;release create(
--tag/--title/--draft/--prerelease) replaces L49-51 and prints the id;release upload --tag --assetreplaces L53-55 and resolves the tag to the id itself (getReleaseByTag,src/cli.jsL1203), so no step has to carryRELEASE_IDto the next.release createalsotakes
--asset, so create-and-attach can collapse into one call.One clause was simply wrong, and it was the one carrying "probably small": the checkout
does not make
node src/cli.jsavailable with no install.src/cli.jsL3 requirescommander, the package's sole runtime dependency; on a bare checkout it exits 1 withMODULE_NOT_FOUND(measured). It is harmless in this workflow only, because L28 alreadyruns
npm ci && npm testbefore the release steps — but the clause is false in general andwas load-bearing for the conclusion, so it is corrected here rather than quietly dropped.
The trap this leaves for whoever converts it:
release.ymlL35 already setsSTOKE_TOKEN, in the step immediately above thecurls, which reads as though the CLI isalready authenticated in this file. It is not.
STOKE_TOKENis honoured by exactly one linein the tree —
scripts/publish-deb.sh:30— and nothing undersrc/ever reads it:src/config.jsresolves the token only from a config file, and everyloadConfig()callsite in
cli.jsgoes through it. ExportingSTOKE_TOKENand calling the CLI yields anunauthenticated client, not a working one.
A second route exists, and this finding had never named it — measured 2026-09-01.
Porting the three
curls to stoke's own CLI is one way; the other is to stop carrying abespoke release workflow at all.
heavy-duty/crewandheavy-duty/provider-seeker— theorg's only other release-shipping repositories — hand-roll nothing, because their
.github/workflows/release.ymlis a four-lineuses: heavy-duty/ceremony/.github/workflows/release.yml@<tag>(crew at0.6.2,provider-seeker at
0.6.3). That reusable workflow contains zerocurlcalls: it goesthrough
lib/forge.sh→forge_select, which detects this forge fromGITHUB_API_URL's/api/v1shape and dispatches to the Forgejo backend, and it authenticates with the ambientGH_TOKEN: ${{ github.token }}rather than a pastedsecrets.RELEASE_TOKEN. Thisrepository's own tree already records the route, and a cost of not taking it:
.ceremony/README.mdL9-14 says stoke "keeps its own tag-driven.forgejo/workflows/release.yml" and that "docs-sync --fixcannot run until or unless afuture issue adopts the ceremony release-workflow pin".
Route B is not free, and its price is named here rather than discovered later. Ceremony's
workflow builds nothing itself — it invokes the consumer's own
.github/actions/release-artifact/action.ymlafter the tag exists and before the publish,with the hook contract drop finished files into
$RELEASE_ASSETS_DIR. crew andprovider-seeker both carry that action; stoke does not (
contents/.github/actions/release-artifact/action.yml→ 404 on stoke, 200 on both siblings), so adopting the pin means authoring one that
runs
scripts/build-deb.sh. Nor does it retiresecrets.RELEASE_TOKENfrom this file:scripts/publish-deb.shPUTs the.debto/api/packages/heavy-duty/debian/pool/…, apackage-registry surface neither route touches. Route A is smaller and keeps the workflow
stoke's own; route B deletes the workflow's body outright and closes the empty-notes defect
below as a side effect. Triage picked route A at the ladder's 24h rung on
2026-09-02, and the work is #54.
The decision, its measurements and the doubt that remains are the comment that
cleared
needs-ruling; the two paragraphs below are what that pick rests on.A fifth cost of B, surfaced at the ladder's 12h rung on 2026-09-01 and absent from the
0.6.3 measurement the recommendation rests on: at the only tag stoke could pin today, B's
publish is not atomic and the hand-rolled block it would replace is. Ceremony's
.github/workflows/release.ymlat tag0.6.3reaches the tag door's publish step with noexistence check — L385 is
forge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}"straight after the artifact hook — and
forge_release_createat0.6.3(
lib/forge-forgejo.sh) POSTs the release withdraft:falsefirst, then uploads eachasset; an upload failure returns 1 with the release already published, there is no rollback,
and because the create is not a get-or-create a re-run of the job meets Forgejo's conflict
refusal instead of finishing the attach. This repository's door behaves better on
exactly that axis. Re-pointed by triage: the citation below named
.forgejo/workflows/release.ymlL47-52 and "this repository's own three
curls", both of which !55 deleted — at088e7e2dgit grep -c curl .forgejo/workflows/release.ymlreturns 0. The property survived theconversion and only moved:
scripts/publish-release.shL44-49is get-or-create (
release view→release upload, elserelease create), so a re-run findsthe existing release and re-attaches, and
scripts/publish-deb.shL42-51 treats 409 asAlready publishedwithout exiting 1, so the registry step is idempotent under the same re-run.(
scripts/publish-deb.sh L42-51is ad6a21c9d/088e7e2dcitation and is spent atmain— keptverbatim, because the ref it is true at is the one this argument runs on. Triage, 2026-09-03.)
!59 merged as
bbde478at 2026-09-03T07:30:12Z and inserted a six-line heredoc above this block, so at
main(
2230ca25) the file is 60 lines, thecaseruns L50-59 and the409arm is L52. At088e7e2d— 52 lines, the commit
v1.5.0points at and therefore the tree a re-run of run 990 replays —it is L42-51 with the
409arm at L44, exactly as written. The claim is unchanged at bothrefs (
409) echo "Already published (409): …", noexit 1), and this citation is load-bearing forthe re-run recovery, so read it at
088e7e2d, not atmain.This is no longer a paper argument. Run
990 on
v1.5.0publishedstoke 1.5.0to the registry and then failed on the next step, which is precisely thestrand-a-partial-release shape the ceremony defect describes — and stoke's door is recoverable
from it by a plain job re-run, with no tag deletion and no hand-publish, exactly because of the
two idempotence properties named here. The measured cost of route B is therefore higher than
this paragraph estimated, not lower: adopting it would trade a door that has now demonstrated
recoverability for one whose create is not a get-or-create. Upstream has
already fixed this and the fix is not in any tag.
heavy-duty/ceremony#271— "publish therelease atomically, so a failed asset upload cannot strand a published, incomplete release" —
merged as !272 (
58ab5036) at 2026-08-30T10:47:55Z and rewrites the function to create adraft, upload, then publish, with rollback and stranded-draft cleanup. Tag
0.6.3(
8f0ef796) was cut 2026-08-26T20:18:16Z;git merge-base --is-ancestor 58ab503 0.6.3is false, so the fix lives only on ceremony
main(VERSION=0.6.4-dev, head91aee7f8), and the tag list is stillx-total-count: 8with0.6.3on top. Measuredagainst
forgejo.heavyduty.builders, not GitHub's same-named repository. The cost istherefore dated rather than structural — a
0.6.4carrying #271 erases it — and it does notreverse anything: it adds to B's side. Recorded here because the 24h rung hands the pick to
triage at 2026-09-02T08:35:16Z and the pick reads this body.
Why this was not already on the record, since the same fix was ruled irrelevant once
before. Triage examined ceremony#271 on 2026-08-30 and filed it as a negative probe — not a
defect for stoke, because stoke pins only the label and issue-flow callers and never reaches
forge_release_create. That ruling was correct and remains correct for the pins stokecarries today; what it did not say is that route B is precisely the change that makes stoke
reach that function. A ruling scoped to the current configuration does not survive the
proposal to change the configuration, and this is the second half of that ruling rather than
a reversal of it.
Re-measured at the 24h rung on 2026-09-02, and both halves came back worse for
B than the paragraph above says.
The fifth cost is three commits, not one. Enumerated with
git log 0.6.3..origin/main -- .github/workflows/release.yml lib/forge-forgejo.shagainst
forgejo.heavyduty.builders:c4315c2"publish Forgejo releasesatomically" (2026-08-30T09:25:43Z, the ceremony#271 fix named above),
734676a"guard tag release publication" (2026-08-30T09:27:08Z), which addsa no published release may exist step to the tag door — the door stoke
would enter — and
7bd331a"resume stranded merge-door publishes"(2026-08-31T11:17:49Z), which replaces the merge door's refuse-on-collision
check with a preflight that can resume a stranded run. All three touch exactly
the surface route B adopts; none is in any tag. Ceremony's tag list on this forge
is still
x-total-count: 8with0.6.3(8f0ef796, 2026-08-26T20:18:16Z) ontop, and
mainis91aee7f8atVERSION0.6.4-dev.And there is a sixth cost, which is structural rather than dated — a
0.6.4does not erase it. Ceremony's tag door asserts that the pushed tag equals the
tree's version verbatim:
.github/workflows/release.yml@0.6.3L347is
if [ "$GITHUB_REF_NAME" != "$ver" ]→ "does not match the tree's version —creating nothing", exit 1, where
vercomes fromversion_read(
lib/version.shL16). Every release tag stoke has ever pushed isv-prefixed —v1.3.0,v1.4.0— and.forgejo/workflows/release.ymlL16-17filters on
tags: ['v*'], whilepackage.jsonholds1.5.0(it read1.4.0when this paragraph was written; #56 bumped it at
088e7e2d, measured2026-09-02T21:4xZ) and there is no
VERSIONfile (GET /contents/VERSION?ref=088e7e2d404, against a 200 control on
package.jsonat the same ref). Under B,v1.5.0would be refused by that assert. The enumeration two clauses up is now short one member — and the claim it backs is still true, which is exactly why nothing caught it (triage, 2026-09-03).v1.5.0was pushed at088e7e2don 2026-09-02T22:29:15Z by @claude-lead-andresmgsl — an annotated tag,6ba87b48, messagestoke 1.5.0— so this repository has threev-prefixed release tags, not the two listed:v1.3.0,v1.4.0,v1.5.0(GET /tags→x-total-count: 3, re-measured this tick).v1.5.0is therefore no longer the hypothetical this clause treats it as: it exists, run 990 fired on it, and its.debis live in the registry while its release page is not (GET /releases/tags/v1.5.0→ 404; releases stillx-total-count: 2, so "two release pages" below is correct and must not be "fixed" to three). The pair is kept verbatim, because the rot is the finding and not the sentence. Every release tag stoke has ever pushed is stillv-prefixed — the universal claim re-verifies as true, so neither a false-statement grep nor a re-check of the conclusion ever reaches the list beneath it. That is the shape: a universal claim with an exhaustive-looking enumeration launders its own evidence. Nothing about the A-vs-B ruling changes — it gets stronger, because the published-artifacts cost this paragraph weighs now spans three tags rather than two. This body already records the push at the two places that own it (the## Notesgate below, and #54's acceptance criterion 6 which states "This repository now has three tags") — as withleaving twoin## Context, the stale copy was the one nobody re-read. Adopting Btherefore means renaming stoke's release-tag convention on a repository whose
artifacts are already published under the old one — the apt registry, two
release pages, and the install documentation — which is the published-artifacts
class this epic's own escalation calls a hard block by construction. crew and
provider-seeker never met this cost because they were on bare
X.Y.Zfrom theirfirst release. Measured, not inferred: both siblings' callers were re-fetched
this tick (crew pins
0.6.2, provider-seeker0.6.3, both carry.github/actions/release-artifact/action.yml, which stoke still 404s).One worry that measurement dissolved, recorded so it is not re-raised: adopting
the pin does not force stoke onto the merge door. Both doors live in the
reusable workflow and the callers keep only triggers and permissions, so a
tag-driven stoke stays tag-driven under B. The tag door is simply the door that
carries the two untagged fixes and the naming assert.
Still not minted — and as of 2026-09-01 the sequencing half has answered itself, leaving
only the route call. The work is specced enough to write; what was not triage's to decide
was when, because converting this file changes the path that publishes stoke's own
artifacts and a release was pending on it. That gate is spent:
v1.4.0was tagged2026-09-01T08:12:57Z at
523a4558,the registry serves
stoke 1.4.0, and #32 is closed. The default this epic recorded —"after
v1.4.0ships" — has therefore fired, and by this paragraph's own terms theconversion becomes mintable as a fresh
readyissue the moment the route is picked. It isminted: #54, created
2026-09-02T09:14:16Z, carrying route A as a decided spec. It was
readyand unclaimedfor three minutes: @codex-bot-andresmgsl claimed it at 2026-09-02T09:17:59Z — read off
the label events and not the thread,
readyremoved 09:17:58Z,claimedadded andself-assigned 09:17:59Z — and opened
!55 at 09:19:44Z. The route
question this epic held is answered; the
needs-rulingit carried is cleared in the samecomment that records the decision. Correction to this paragraph's own prose: it said
the flag went up "from 2026-09-01T08:40Z", which was approximate — the
labeledevent is2026-09-01T08:35:16Z, it is the only one this issue has ever had for this label, and it
is what both the ladder and
reconcile_rulinganchor to.And the tagged run gave the route call evidence it did not have when it was written.
Run 735 — the first
time
release.ymlhas ever fired — went red: 117/117 tests passed and the.debbuilt,then the Publish to Debian registry step exited 1 at
scripts/publish-deb.sh:33with
error: no token, becausesecrets.RELEASE_TOKENexpanded to empty. The Createrelease and attach .deb step — the three hand-rolled
curls this finding is about — neverexecuted, so
v1.4.0was published by hand. This does not decide A vs B, and it isimportant that it is not read as doing so: the paragraph above already measured that
scripts/publish-deb.shPUTs to a package-registry surface neither route touches, sothe absent secret bites identically under both and is an operator act either way. What it
does establish is that this file has now demonstrably failed in production, which is a fact
the route call did not have on 2026-08-30. #32's close comment carries the full
measurement so the two do not drift apart.
A second, independent thing about the same three
curlcalls, recorded2026-08-30T15:47Z. The create at
release.ymlL49-51sends
{"tag_name":"$TAG","name":"$TAG","draft":false,"prerelease":false}andno
body— and the step reads no file for one — so every release thisworkflow publishes has an empty notes pane. That closes a loop:
scripts/build-deb.shL60-66writes the
.deb's ownchangelog.gzas the single bullet "See the repositoryrelease page for notes." The installed package directs users to a page that is
blank by construction.
Not minted, and not folded into #32 — and this reason has narrowed too, though it has not
vanished. Sending a body needs this release's section extracted from a
CHANGELOG.md— ceremony does it withlib/changelog.sh, this repo has noequivalent, and until #32 gained the task on 2026-08-30 it had no
CHANGELOG.mdat all. The half of this that pointed at the
curlcalls is spent:release createtakes
--body-file <path>(src/cli.jsL1147-1148, read byreadBodyOptionL62-71), sosending a body is a shipped capability and no longer waits on the conversion question
above. What remains is only the extraction —
--body-filereads a whole file verbatim, itdoes not select a section — so the missing piece is a
CHANGELOG.mdsection-extractor, andit is independent of whether the workflow is converted. That extractor was never looked
for, and it already exists — measured 2026-09-01. Ceremony ships
bin/changelog-section <version> [file], which sourceslib/changelog.sh; its matcher isawk '/^## / && $2 == ver'. Both files were fetched at tag0.6.3fromforgejo.heavyduty.buildersand run against this repository'sCHANGELOG.mdat9586d2c, unmodified:changelog-section 1.4.0→ exit 0, 21 lines, the complete### Added/### Changed/### Fixedbody on stdout, nothing on stderr. The heading## 1.4.0 — 2026-08-31matches because the date lands in$3, not$2. So the extractionis no longer unspecced under either route: under A it is a vendor-or-reimplement of ~30
lines whose output goes to
release create --body-file; under B it is nothing atall, because ceremony's release workflow already publishes the version's own
CHANGELOG.mdsection as the release body and refuses to publish a release whose section is missing. Two
changes to one file, and neither is now waiting on anything unspecced — only on the route
call above. #32's Spec now carries the
matching record, and its post-merge criterion reads "the artifact is attached",
never "the notes are published" — so the fact sits both on the issue that
publishes and on the epic that owns this file's open question, and neither can
go stale alone.
What this epic does not do
It does not re-file or close anything. #1 and #23 keep their numbers; their
bodies were revised by triage on 2026-08-18 per these findings, so both are
now buildable as specced. #21 was a PR, not an issue, and was out of scope here;
it closed unmerged on 2026-08-30 and the part of it that belonged to #1 was folded
into #1 by triage that day. (An earlier
revision of this section also named #22; that number resolves to nothing
on the forge — a deleted PR — and is not part of this epic.)
Task list
Dependency-ordered. #26 and #24 are independent and either may go first.
#26 —
issue create --label(P1)#24 —
repo create --owner(P1) — merged 2026-08-21 as !35; #24 closed#25 —
release create --asset+ print the release id (P2) — merged 2026-08-30 as !37 (033a40c); #25 closed. Built by @codex-bot-andresmgsl; all six of its acceptance criteria were pre-merge, so theCloses #25was correct and nothing was owed topost-merge#1 — rescoped to the
[trusted=yes]verification gap (P2) — merged 2026-08-31 as !38 (fb5cb474); #1 closed the same second. Built by @codex-bot-andresmgsl, claimed 2026-08-30T11:28Z; !21 had closed unmerged and its fallback narrowing was folded into #1 rather than re-minted. The panel approved at the merged head2efc76f2(@kimi-bot-andresmgsl 15:27:49Z, @claude-bot-andresmgsl 15:28:16Z, @glm-bot-andresmgsl 15:28:17Z, none stale). The close was @andres's hand close, not the derivedpost-mergemove — !38 saysRefs #1and no commit in the merged range carries a close keyword — so triage wrote the transition record instead (comment 30740 on #1) and released the claim there. One criterion outlived the merge, "release automation publishes on every version tag": it is unfired because nov*tag has ever been pushed here, and it is carried on #32 as post-merge criteria 1-2 with the identical wake condition, so this close strands nothing.Triage annotation, 2026-09-03 — this row's last sentence makes three claims and all three
have inverted; the wake it calls unfired fired on 2026-09-01. Original kept verbatim. The
sentence reads "it is unfired because no
v*tag has ever been pushed here, and it iscarried on … as post-merge criteria 1-2 with the identical wake condition". Corrections, in
the order they went false:
The wake fired 2026-09-01T08:12:57Z.
v1.4.0was pushed at523a4558andrelease.ymlran on it as run 735, the first time that workflow had ever fired. It went red 23 seconds in
at
scripts/publish-deb.shL33 —error: no token, becausesecrets.RELEASE_TOKENexpandedto empty — so Create release and attach
.debnever ran, andstoke 1.4.0reached theregistry and its release page by hand. A second
v*tag has fired since:v1.5.0at088e7e2d, 2026-09-02T22:29:15Z, run 990, red at that same step for a different reason(
403 … required scope(s): [read:user]). Those two are still the only runsrelease.ymlhasever had, and both failed. The criterion is therefore measured false, not unfired, and it
has been for two days.
"No
v*tag has ever been pushed here" compressed a careful sentence into a false one.v1.3.0was pushed 2026-07-26T21:37:32Z, long before this row existed. The source claim onthe issue this row is about states the precise thing instead — that
v1.3.0predates theworkflow, so no tag had yet fired it — and runs 735 and 990 have falsified that narrower
reading as well.
The carrier closed. The release 1.4.0 issue this row hands the criterion to was closed
2026-09-01T08:36:10Z with the exact criterion named here — its post-merge criterion 1,
"
v1.4.0is tagged by @andres andrelease.ymlruns on it" — still unchecked. Thestructure this row cites as its proof that the close stranded nothing no longer exists.
The conclusion nevertheless survives, by a route this row does not name. Triage answered
the criterion on the closed issue itself at 2026-09-01T08:36:27Z (comment 32295): the tag,
the run, the failure, the negative resolution, and the remainder handed on as the release
door rather than the apt install path this row is about. Nothing is stranded — but a reader
who checks only this row cannot learn that, and would conclude the opposite of what happened.
The shape, because it is new on this board: "it is carried on " is a
live-state claim about that issue, not a fact about this text. It survives every check that
greps this body — the number resolves, the destination really does contain the two criteria,
and the destination's own Dependencies section still reads "which is why nothing was
stranded by closing it" — so the cross-reference confirms itself while being closed and
unticked. And a close merges nothing, moves no queue and mints no label, so none of this
board's wake conditions could see it. Re-check every "carried on" cross-reference in an open
body whenever any issue on this board closes.
#23 —
repo sync, spec narrowed per finding 2 (P3) — merged 2026-08-31 as !41 (c34a8b04); #23 closed the same second by itsCloses #23. Built by @codex-bot-andresmgsl, claimed 2026-08-31T16:41Z. The panel approved at the merged head316bec58(@glm-bot-andresmgsl 18:29:36Z, @claude-bot-andresmgsl 18:32:06Z, @kimi-bot-andresmgsl 18:32:14Z, none stale). All five acceptance criteria were pre-merge, so theCloseswas correct and nothing was owed topost-merge; the derived move therefore never ran, and triage wrote the transition record and released the claim by hand (comment 30976 on #23). This row records the transition, not the live state.#54 —
.forgejo/workflows/release.yml: convert the three hand-rolledcurls to stoke's own CLI and publish the version'sCHANGELOG.mdsection as the release notes (route A). Minted 2026-09-02T09:14:16Z, at the ladder's 24h rung, by the triage pick that this row could not exist before. Claimed by @codex-bot-andresmgsl 2026-09-02T09:17:59Z and built as !55, opened 09:19:44Z, whose body opensRefs #54— so the close is triage's and not the merge's. Merged intomain2026-09-02T11:22:58Z asd6a21c9d, by @andres, at headee88d7d3. The sweep derived the transition at 11:37:30Z —claimedremoved, the assignee released,post-mergeadded — so the live labels are nowenhancement+post-merge+scope:packaging. It was the whole of acceptance criterion 2 below, which ticks on this merge. This row stays unchecked because #54 is still open: its one surviving criterion is post-merge and operator-gated, and triage owns that close. TICKED 2026-09-03T21:20Z — #54 is closed, and so is this epic's last open child. Its onesurviving criterion (AC6) was discharged by run 1106,
2026-09-03T20:52:30Z: the converted door published
v1.5.0end to end — release id 66,stoke_1.5.0_all.debattached at 20:52:44Z, and the notes pane carrying the 713-byte1.5.0CHANGELOG.mdsection produced bychangelog-section.sh. The log takes the create branch,so nothing was hand-made. Verified against the forge by triage, not read off the run's own
say-so.
Triage annotation, 2026-09-03 — the label enumeration in this row was exact when the
derivation was recorded and is now incomplete. Original kept verbatim. The three labels
named are what the sweep's 11:37:30Z transition left on #54. Triage itself added a fourth,
scope:ci, at 2026-09-03T01:54:59Z — read off the label events — so the live set isenhancement+post-merge+scope:ci+scope:packaging. Nothing in the transition recordabove is wrong; what rotted is the word now. An enumeration of a live label set claims
completeness, so an ADDITION falsifies it — and a label mint moves no queue, touches no
file and merges nothing, so none of this body's merge-keyed wake conditions could see it.
Read the clause as "the sweep's derivation left
enhancement+post-merge+scope:packaging",and take the live set from the label events, never from a body. The row immediately above
states the discipline this one missed: "This row records the transition, not the live state."
Every issue this list references except #54 is closed — the five completed rows above plus #32,
which the sweep reaches through the prose inside the #1 row (
epic_referenceshandseach whole row to the state check, so any
#Nin a row counts as a child). The sweepnudged this epic as complete on 2026-09-01T08:36Z and again through 2026-09-02T09:00:26Z
(run 894), on the reading that every referenced child was closed; that nudge should now
stop of its own accord, because #54 is an open child in the row above. All four acceptance criteria below are
now ticked, criterion 2 as of !55's merge. This epic nonetheless stays open, and for a
narrower reason than before: its last child #54 is still open at
post-merge, holding oneoperator-gated criterion. Half of that gate is now spent, 2026-09-02T19:33:47Z: @andres set
RELEASE_TOKENat the heavy-duty org level, so the criterion waits only on the nextv*tag. That half is spent too, and it did not discharge the gate (triage, 2026-09-02):v1.5.0was pushed at088e7e2don 2026-09-02T22:29:15Z, run 990 fired on it, and #54's criterion still does not read. The wake is now a re-run of run 990's job under a token that also carriesread:user— see #60 — not another tag. Do not read the sentence before this one as still live; it is kept because it is what the board was told, and the correction is the finding.BOTH SENTENCES ABOVE ARE NOW SPENT, AND THE SECOND ONE WAS NEVER PERFORMABLE (triage,
2026-09-03T21:20Z). "The wake is now a re-run of run 990's job" named an act that does not
exist on this Forgejo:
POST …/actions/runs/990/rerun,…/actions/tasks/990/rerun,…/actions/runs/990/rerun-failed-jobsand…/actions/runs/990/jobs/0/rerunall return 404(probed 2026-09-03T21:1xZ). Triage wrote that instruction onto four bodies — this one,
#54, #56, #60 — and in the same breath
forbade the only mechanism that works: delete the tag and re-push it. That is what @andres
did, twice — run 1104 (
failure, missingwrite:repository) and run1106 (
success), bothpushevents onv1.5.0at088e7e2d. It washarmless:
refs/tags/v1.5.0still resolves to the same annotated tag object6ba87b48acff896d47065a24b0cde7fe3c0d3373at the same commit with the same tagger and date, andpublish-deb.sh's409arm protected the re-push exactly as it would have protected a re-run.#54's AC6 is met, #54 is closed, and this epic closes with it under the contract stated two
sentences below. #56 and #60 closed on the same run in the same tick. The lesson, recorded
here because this is the epic that will be read next time: a wake condition names an act —
verify the act is performable before writing it onto four bodies. An unperformable instruction
does not stall the work, it gets routed around, and then the board's record is wrong about how
the work got done.
That tag has a vehicle, and as of 2026-09-02T20:18:58Z the vehicle has landed —
#56, release
1.5.0, whose PR!58 merged into
mainas088e7e2d. Its five pre-merge criteria are verified at that commit, the sweep derived itstransition at 20:47:58Z, and it now reads
post-mergeand unassigned with only its threetag-gated criteria left — the same wake condition #54 has. Two, not three, and not tag-gated,
since 2026-09-02T22:29Z. The tag criterion is met —
v1.5.0resolves to088e7e2dandrelease.ymlran — and the third criterion's registry half is met as well:GET /api/v1/packages/heavy-duty?type=debianreturnsdebian stoke 1.5.0, created 22:29:42Z. What isleft is the release page carrying the
CHANGELOG.mdsection as its notes withstoke_1.5.0_all.debattached (
GET /releases/tags/v1.5.0→ 404, re-measured this tick), and a clean-containerinstall. Both wait on the re-run: run 990 is
failure, and it died at Create release and attach.debon403 … required scope(s): [read:user]after the registry step had already returned201. #54's AC6 shares that identical wake, so the two still move together — which is the onlypart of this sentence that survived the tag. (This paragraph said
readyandunassigned when it was written at 19:50Z, four minutes before a builder claimed it; corrected by
triage 2026-09-02T20:49Z.) #56 is not a child of this epic, on the
same reading that kept #32 out of it: this epic's scope is the five CLI gaps, not shipping. It
earns no row in the task list above and does not change what this epic closes on. The epic is
#54's progress view, so it closes when #54 closes and not on its own criteria — triage
owns both closes. Nothing is stranded either way: #54's residual criterion carries its own
owner and wake condition in its own body, exactly as #1's did when it was carried to #32. The route question is closed: the ladder's 24h rung arrived at the
labeledevent's 24-hour mark, 2026-09-02T08:35:16Z, the machine posted it at 09:00:26Z, and past
24h the pick is triage's — it picked A, recorded the decision, minted #54, added the
row above and cleared
needs-ruling, all in one comment. Recorded 2026-09-01T08:49Z; thesixth row and this paragraph revised 2026-09-02.
Acceptance criteria
findings here.
curlwith a pasted tokento create an org repo, label an issue, or attach a release asset.
Ticked 2026-09-02 on !55's merge (
d6a21c9d), which deleted the lastone. The CLI could do all three as of
033a40c(2026-08-30), butrelease.ymlL47-55 went on hand-rolling the asset attach until thatmerge; the step is now one line delegating to
scripts/publish-release.sh.See finding 4. The words "this org" went unmeasured for the life of
this epic; measured 2026-09-01, and the criterion is bounded by one
file. Every workflow, composite action and
scripts/bin/cli/libfile in all 9 repositories of
heavy-dutyonforgejo.heavyduty.builders was scanned — 91 files, matching
curlwith a four-line window so multi-line invocations are not missed (a
line-anchored grep did miss one). Four files carry a token-bearing
curl, and exactly one performs any of the three operations above:heavy-duty/stoke.forgejo/workflows/release.ymlL47/49/53 — inscope, and the whole of it. The other three are out of scope and are
listed so a converter does not sweep them in:
heavy-duty/stokescripts/publish-deb.shL38, aPUTto/api/packages/heavy-duty/debian/pool/…— a package-registry surfacethat is none of the three and for which
src/cli.jshas no commandgroup at all (no
packagecommand; the file's only registry token isthe
read:package/write:packagescope string at L154);heavy-duty/ceremony.github/workflows/labels.ymlL175/L190, adefault-branch read and the sweep dispatch, on the ambient
GITHUB_TOKENand not a pasted secret; andheavy-duty/ceremonylib/forge-forgejo.shL143/160/532/664/851, the engine's own forgetransport — the very thing consumers exist in order not to
hand-roll. Both routes in finding 4 move stoke's calls into a transport
of that kind rather than eliminating HTTP, which is what this criterion
asks for. As of 2026-09-02 the whole of it is
#54,
minted when triage picked route A at the ladder's 24h rung, merged as
!55 on 2026-09-02T11:22:58Z. Nothing else was owed against it.
The tick was re-measured, not inherited. Triage re-derived the scan
rather than trusting the 2026-09-01 figures: of the 9 forge repositories
only
ceremony,office-equipmentandstokehave default-branchcommits dated on or after 2026-09-01, so those three were re-scanned in
full at their live heads and the other six are unchanged since the
original measurement. Result:
stoke(d6a21c9d) now carries onetoken-bearing
curl,scripts/publish-deb.shL38 — the out-of-scoperegistry
PUT— and.forgejo/workflows/release.ymlno longer appearsat all;
office-equipment(82e32a9c) carries none;ceremony(
91aee7f8) carries the same two out-of-scope files as before, thoughtheir line numbers have drifted with the tree
(
lib/forge-forgejo.shnow L143/160/532/536/664/855, recorded here on2026-09-01 as L143/160/532/664/851). No in-scope hand-rolled call
remains anywhere in the forge org.
(Both
scripts/publish-deb.sh L38citations in this criterion — the out-of-scopelist above and the re-measurement here — are spent at
main. Triage, 2026-09-03; keptverbatim, and noted here because this is the paragraph that corrected a sibling repo's
drifted line numbers and has since acquired its own.)
!59 merged as
bbde478at 2026-09-03T07:30:12Z, replacing
publish-deb.sh's one-line empty-token guard with asix-line heredoc and pushing everything below it down by 8 lines. The token-bearing
PUTis L38-40 atd6a21c9dand at088e7e2d(52 lines) and L46-48 at2230ca25(60 lines), with theAuthorization: tokenheader on L47. Themeasurement is untouched — only the two
L38s moved: atmain,grep -c 'Authorization: token' scripts/publish-deb.shis still 1, it is still thepackage-registry
PUT, and it is still out of scope. Re-resolved in the same pass andunchanged:
src/cli.jsL154 is still theread:package/write:packagescope string,and
heavy-duty/ceremonyis still at91aee7f8withlib/forge-forgejo.shcurls atL143/160/532/536/664/855 exactly as corrected above.
Host note, same trap as
heavy-duty/ceremonyon #36:heavy-dutyis itself two organizations — 9 repositories onforgejo.heavyduty.buildersagainst 28 ongithub.com, sharingfive names (
box,rig,cast,ceremony,crew) — andstokeexists on the forge only. The scan above is the forge org, which is the
one this repository belongs to and the one all three operations run
against.
apt-get install stokeon a clean container verifies signatures viasigned-by=, and no[trusted=yes]is written by any path the user didnot explicitly ask for. Rescoped 2026-08-30 by #1's ruling (option B):
on gpgv-based apt this reads as written, while on sqv-based apt — where
this registry's
InReleaseis authentically signed but unparseable — thedocumented install refuses rather than downgrading, and the opt-in for
a user who accepts HTTPS-only integrity is the sole route to an unverified
source. The epic's own test plan below inherits the same split. Held to
#1's transcripts, both containers. Ticked 2026-08-31 on !38's merge
(
fb5cb474): @codex-bot-andresmgsl's transcripts on #1 cover both — Ubuntu24.04 installs with the
signed-by=source retained, Debian 13 refuses andleaves no source behind, and the opt-in is the only route to
[trusted=yes]. Two honest limits on this tick, neither of whichcontradicts it: those transcripts were taken at
9cc9576, one commitbefore the merged head, and the delta at
2efc76fonly narrows theopt-in gate (the broad signature-class regex was removed) while adding the
negative boundary test; and triage re-read the merged tree itself rather
than taking the PR's word —
git grep -in 25519is empty and the single[trusted=yes]write atscripts/install-apt.sh:164sits behind both theexact
/usr/bin/sqv+Malformed MPIcondition andSTOKE_ALLOW_UNVERIFIED_APT=1.sync / documented merge / pull-mirror) it implements, and which it
explicitly does not.
Test plan
Each issue carries its own. Epic-level, the proof is a clean container that
installs stoke via a signature-verified apt source and then runs, with no raw
curl: create an org repo, mint a labelled issue, cut a release with anasset attached.
Dependencies
None blocking. Related: ceremony#197 (the ceremony sync epic, closed) — finding
2's evidence lives there. The diverged-tree procedure finding 2 defers to is no
longer a pending issue: it was delivered as ceremony's
docs/UPSTREAM-SYNC.md(ceremony#200, closed 2026-08-05), which carries the standing resolutions each
sync would otherwise re-decide. That link now tracks
mainrather than the2026-08-05 commit it was pinned at: the doc gained a 0.6.2 port record on
2026-08-24 and, on 2026-08-27, the operator's ruling that the next sync
campaign merges rather than ports (ceremony#269). Cited as live doctrine, it
is re-pinned to the branch so it cannot freeze again. #23 now points at
that doc rather than at the closed issue.
Triage, 2026-08-18 — board normalization. This repo had no label taxonomy at all: every open issue sat label-less, invisible to any queue scan. Done in this pass:
heavy-duty/ceremony's definitions. Noscope:*labels were minted: the scope set is per-repo config that lives in.github/labels.conf+CONTRIBUTING.md, and stoke has neither — adopting ceremony governance files (including the labels workflow, which normally owns this bootstrap) is operator work, noted here rather than invented by triage.epic; #26, #24, #25 →ready; #23 →readyafter its spec revision (finding 2); #1 →blocked(by PR #21) +needs-ruling(anonymous registry access — escalation on #1).## Task listfor sweep visibility.Open PR #21 is untouched — it is the reviewers' surface, and it now gates #1.
Triage — hygiene tick, 2026-08-19. Two acceptance criteria are now met and have been ticked in the body; the rest of the epic is re-verified against shipped code rather than against its own prose.
Ticked, with the evidence:
[trusted=yes]verification gap and #23 was narrowed to the undiverged-mirror verb, both on 2026-08-18 (the triage comments on each issue record the rewrite). Findings 1 and 2 are the only ones this epic raises, and both now live in the bodies they are about; #24, #25 and #26 stand as measured.release create+ #25.Not ticked, and why — both remaining criteria need code that has not landed: no workflow-without-curl (that is #24, #25, #26 shipping) and no signature-verified clean-container install (that is #1, whose blocker !21 is red and stalled — see the comment there).
Children re-verified against
main@ee0cb85, not against their own text, so no label here is stale and nothing is obsolete:mainissue createcannot set labels--labelabsent; PR !29 carries it, unmergedclaimed, PR open, approved 2 of 3repo createhas no--ownersrc/cli.jsL424–432: name, description, private/public, auto-init, default-branch — no ownerreadyrelease createcannot attach assetssrc/cli.jsL1052–1061: tag, target, title, body, draft, prerelease — no--assetreadyrepo syncsyncsubcommand existsready[trusted=yes]gap unclosedblockedTask list unchanged: nothing has landed, so nothing is checked. #26 is the closest — its PR is green and needs one more panel request, not more work.
Out of scope here and stated so the epic is not read as covering it: #30 (adopting ceremony's labels machinery) is governance, not one of the five CLI gaps, and carries no dependency edge to this epic in either direction.
Triage sweep, 2026-08-20 — epic refreshed, children brought to contract. The task list here was already accurate; the Context paragraph was not, so it now records that #26 merged as !29 and closed on 2026-08-19, leaving four of the five open. No checkbox state changed: #26 stays ticked, and the second acceptance criterion stays unticked because it is conjunctive — labelling an issue no longer needs a hand-rolled
curl, but creating an org repo (#24) and attaching a release asset (#25) still do.Both P1/P2 children were
readyon bodies that did not meet the issue contract, so the label was ahead of the spec. Fixed in place, original text untouched, contract sections appended:Part of #27edge, though its fix was already decided. Retitled to name the deliverable.--asset-name's single-asset restriction and partial-failure behaviour. Specifying it surfaced the real work: the API client has no multipart path at all and its 30 s timeout would abort the 29/50 MB payloads that motivated the issue. Retitled likewise.#23 was already at contract from the 2026-08-18 rescope and is unchanged. #1 stays
blocked— !21 is open withblocker:ci-redand a standingREQUEST_CHANGESat head33079afb, and #1's own first task is that it lands — but a false claim in its body was corrected: the instance does have a registereddockerrunner, so its last unmet original criterion wakes on av*tag push, which #32 produces. Details on #1.The three open children remain independently claimable; three distinct verbs are three distinct deliverables, so no collision edges apply between them.
epic: the five open CLI gaps — priority order, and two that need rescoping before they are buildableto epic: the five CLI gaps — priority order, and the two spec rescopes (#1, #23) that made them buildableTriage — epic status surfaces corrected, 2026-08-20.
This epic contradicted itself. Its prose says plainly, under What this epic does not do, that #1'''s and #23'''s bodies were revised by triage on 2026-08-18 per these findings, so both are now buildable as specced — and acceptance criteria 1 and 4 are ticked on the strength of exactly that. But the two surfaces a reader actually scans for status still described the rescoping as work someone still owed:
Verified against the issues themselves before rewriting, not against this epic'''s own claim: #1'''s Context reads "This issue is rescoped to the remainder" and carries the narrowed Tasks/criteria plus an Original criteria, status as measured block; #23'''s Context records the finding-2 narrowing and carries the Explicitly out of scope section naming all three shapes (diverged trees → ceremony#200, pull-mirrors, release mirroring). Both are at contract. The rescopes are done; only the build remains on each.
Changed:
No checkbox state changed: #1 and #23 remain open and unticked, because the builds have not happened. Findings 1 and 2 are left verbatim — they are the record of why the rescopes happened, and rewriting them would erase that.
Generalizable: an epic that records its own completed work in prose but not in its checklist reads as stale in the direction of more work owed — the opposite of the usual rot, and invisible to a label-only check.
Triage — Dependencies repointed, 2026-08-20.
Same defect as the one repaired on #23 this tick, in this epic's live pointer:
Dependencies read "ceremony#200 is where the diverged-tree procedure belongs",
future-tense, but ceremony#200 closed 2026-08-05T19:12:40Z and delivered
docs/UPSTREAM-SYNC.md(verified present on ceremony
mainc2ef6a2this tick). Dependencies now namesthe doc; ceremony#197 is marked closed there too.
Finding 2 is left verbatim, including its "That work is ceremony#200, and it
belongs there, not here" line. The findings are the record of why the rescopes
happened and rewriting them erases history — the same rule applied to findings 1
and 2 during the 06:30Z surfaces repair. Only the live pointer moved.
Finding 2's premise re-verified rather than assumed:
docs/UPSTREAM-SYNC.mdstates the two ceremony trees "diverge in opposite directions on purpose", so the
divergence that motivated narrowing #23 is standing, not resolved by the 0.6.0
merge landing.
No checkbox state changed, no label changed (
epic, set 2026-08-18T00:23:51Z, nolabel event since — correctly carries no queue label). Task list re-verified
current: #26 ticked and closed, #24/#25/#1/#23 open, #1 still
blockedon !21.Triage — finding 3 added, and the task list now records a serialization, 2026-08-20.
No child's spec, tasks, criteria or estimates changed; no checkbox flipped.
#23, #24 and #25 are one collision cluster — all three edit
src/cli.js(a single1653-line file carrying every command group),
src/api.js, andREADME.md— and all threecarried
readyfrom one batch write on 2026-08-18T00:23:52Z with no edge between them.Their bodies recorded the shared file as Related prose, which constrains nothing, so the
board offered three concurrently-claimable issues that contend for the same file.
Resolved by serializing the cluster in this epic's own priority order — #24 (P1) → #25
(P2) → #23 (P3) — rather than mint order (#23 → #24 → #25), which would have gated both
smaller, higher-priority issues behind the largest one and inverted the priority table
above. #24 stays
readyas the single source; #25 and #23 moved toblockedwith orderingedges only.
The task list rows now carry each child's cluster position, so a scan of this epic shows
which single child is claimable. #1 and #32 are outside the cluster and unaffected.
Verified against ceremony's blocker parser: #24 →
{}, #25 →{#24}, #23 →{#25}, andthis epic →
{}(finding 3's prose is deliberately written so the marker phrase does notparse — an epic must not declare a gate it does not have).
Triage — finding 3 corrected in the body, and the task list restored, 2026-08-20.
Earlier this tick I added a finding 3 that called #23/#24/#25 a collision cluster,
serialized them #24 → #25 → #23, and moved #25 and #23 to
blocked. That was anover-application of the rule. All three are back to
ready, the task-list rows and the"either may go first" line are restored verbatim, and no child's spec, tasks, criteria or
checkboxes were ever touched.
Why the edge was wrong. The collision rule
(#288) governs one deliverable carried by two issues, not any two issues that touch one
large file, and its "disjoint regions do not waive it" clause means regions of that shared
deliverable. I read "all three touch
src/cli.js" as the collision and it is not one.The sweep that enforces the rule settles it:
deliverable_key(issueflow-reconcile 0.6.1,L433) keys on the title's em-dash prefix, normalized and case-folded. The three keys are
repo sync(#23),repo create --owner(#24) andrelease create --asset / release upload(#25) — three distinct deliverables, so the collision flag would not fire on this trio, and
it would be right not to.
Ordinary same-file contention between distinct deliverables is a rebase — the builder's
normal cost, already carrying its own PR label (
blocker:conflict). It is not a board gate,and making it one would have left two smaller, higher-priority issues unclaimable behind the
largest for no doctrinal reason.
Finding 3 now records the corrected conclusion — the shared files, the three distinct
deliverable keys, and why no edge is owed — so a future tick does not re-run the same
analysis and reach the same wrong answer. The shared-file fact stays worth having if this
repo ever adopts a version epic, since release-init graphs same-file clusters when it orders
waves; no window stands on stoke today.
Triage — task list brought current. One child landed, one is claimed, one changed queue state; nothing here needs a decision.
repo create --owner: ticked. Merged as !35 at 2026-08-21T23:18:38Z (c09943e), #24 closed the same minute.repo createonmainnow takes the owner. That is the second of the five gaps closed, after #26.release create --asset: claimed 2026-08-30 by @codex-bot-andresmgsl; !37 is open atstate:addressing. Left unticked — the checklist tracks landings, not claims — but noted in the row so a scan does not read it as untouched.blocked. !21 closed unmerged on 2026-08-30 with its head unmoved since 2026-07-26, so #1's dependency edge was dropped and the[trusted=yes]fallback narrowing folded into #1's own Tasks. Finding 1's closing sentence ("PR #21 may already carry part of it and should be assessed first") and the "what this epic does not do" note both described !21 as open and have been corrected. Details on #1.ready, unclaimed, spec still as narrowed by finding 2.Epic-level acceptance criterion 2 — "no workflow needs a hand-rolled
curl… to create an org repo, label an issue, or attach a release asset" — is now two-thirds met and stays unticked until #25 lands. Criterion 3 (signed-by=install) waits on #1. Findings 1–3 are unchanged and were not re-litigated.Triage, 2026-08-31T11:35Z — the task list said !38 was a draft; it left draft 31 minutes before this tick opened. Three bodies de-rotted, no labels touched.
!38 undrafted at 2026-08-31T11:04Z and ran its first complete panel round in the ten minutes after. Nothing fires an event on this epic when a PR's draft flag flips, so the row went on asserting a state that had stopped being true.
What was false, and where
state:building"state:addressingsince 11:13:51ZThe #32 one is the one that pointed a reader the wrong way. That paragraph exists to decide whether !38's
changelog.d/1.mdlands before #32's branch point, andstate:buildingreads as still being written. It isstate:addressingat a green head with two approvals — much closer to merging than the label named. The label descriptor is deleted rather than refreshed: the paragraph's durable rule (git ls-tree --name-only <merge-base> changelog.d/, re-read not trusted) already answers the question without it, so there is now nothing there to rot.The round, measured rather than inferred
Head
9cc9576, all three requested panelists verdicted:Round complete, not fully approved ⇒ ordinary builder-owed fix.
state:addressingwas set at 11:13:51Z andforgejo-actionsclearedstate:bots-reviewing19 s later, so the label engine and the board agree. Noattentionis owed — this is a review outcome on the PR, not a coordination gap, which is the same call made on !29 and !31.One thing recorded because it is not visible from the labels: !38's acceptance criteria were repaired at 10:20Z by @claude-lead-andresmgsl, not by the claimant, who had been silent on this forge since 06:19Z and resumed at 10:43Z. The claim was left with @codex-bot-andresmgsl and is not reclaimable — six of the branch's seven commits are theirs, the last two are from this morning, and it carries an open PR.
Checked and left alone
blocked→ready: nothing to flip. Zeroblockedanywhere atstate=all.claimed; open PR, activity minutes old.readyissues re-verified as still-real gaps at25c7267e— #23 (nosyncinrepo), #32 (v1.3.0..main= 39, 10 merges, only tagv1.3.0), #33 (12kimi-reviewer-andresmgslhits inREADME.md), #36 (all three remaining items live).0.6.1copy right now". Left as written: it sits inside a section stamped "Recorded by triage 2026-08-30T20:12Z" and the section immediately below it already records that the re-vendor landed andBUILDER.mdat0.6.3is what codex reads now. A dated measurement ages; it does not rot.mainhas not moved since25c7267e, so.forgejo/workflows/release.ymlL47-55 still hand-rolls the asset attach.Triage, 2026-08-31T13:04Z — one word in this epic became a term of art on 2026-08-30, and this body then contradicted itself with it. One sentence reworded; no finding, task, criterion, checkbox, or label changed.
Finding 4's "not minted" paragraph ended "belongs beside #32's release window rather than inside it", while the Finding 3 paragraph forty lines above states "stoke runs no version epic and no release window stands." Both were written under the
0.6.1doctrine this repo vendored at the time, where release window was prose.What changed under them. The out-of-band re-vendor at 2026-08-30T22:15Z (
125e44a…25c7267, recorded on #36) moved.ceremony/to ceremony0.6.3, and0.6.3'sRELEASES.mdadds a## The membership recordsection — now at.ceremony/RELEASES.mdL43 in this tree — which makes window stands a machine-read predicate: an openrelease-labelled issue whose membership record, under a literal## Membersheading, holds at least one open member. It states "there is no fallback to the gate." The matching parser ismembership_references()in the engine this repo's callers now pin.Measured before rewording, not assumed:
release-labelled issuesready), #1 (claimed)## Membersheading in either bodyrelease windowelsewhere on the open boardHEAD is now at 8f0ef79(0.6.3),labels: reconciled./issueflow: reconciled., no window flag, noblocked declarations parse tolineSo no window stands — the Finding 3 sentence is the one that survives, and the label state is right as it is.
Why this earned an edit even though the conclusion did not change. The two sentences now disagree in a way a machine can act on. A reader taking "#32's release window" literally would conclude #32 owes a
## Membersrecord, and writing one is not bookkeeping: it is what makes a window stand, after which everyreadyissue owes a membership row and an unrowed one reads to the sweep as an unblocked non-member. The cheapest fix is the sentence that was only ever loose prose, so Finding 4's wording now names #32, the 1.4.0 release work and says plainly why it was reworded.Not done, deliberately: no
## Memberssection was added to #32 or #1, and none should be.RELEASES.mdputs that record's first write at release-init, and is explicit that a repository whose epics predate it "gets silence, never a false flag." stoke runs no version epic. Finding 3's own sentence is left exactly as it stands.Rot direction this is an instance of, for the record: a re-vendor can invalidate bodies by creating a term of art — no label moves, no issue receives an event, and no forward-reference or label-diff check can see it. The trigger to watch is a doctrine bump that adds a parser, not one that renames a label.
Triage, 2026-08-31T16:41Z — #1 landed; epic updated. Tick record for the whole board.
!38 merged 16:05:04Z (
fb5cb474, head2efc76f2; @kimi-bot-andresmgsl, @claude-bot-andresmgsl and @glm-bot-andresmgsl all APPROVED at that head, none stale), and @andres closed #1 by hand one second later — !38 saysRefs #1and no commit in the merged range carries a close keyword, so the derivedclaimed→post-mergemove never ran and no transition comment was written. Per TRIAGE.md, triage owed that record on a hand move: it is comment 30740 on #1.Edits here. The #1 task-list box is ticked; the P2 row reads landed; the parenthetical that still called #1 claimed now names all three closes; and the apt acceptance criterion is ticked, held — as it always said it would be — to #1's transcripts for both containers, with two limits named in the criterion itself rather than glossed: the transcripts were taken at
9cc9576, one commit before the merged head, and that commit's delta only narrows the opt-in gate while adding a negative boundary test. Triage re-read the merged tree rather than taking the PR's word —git grep -in 25519is empty tree-wide, and the single[trusted=yes]write atscripts/install-apt.sh:164sits behind both the exact/usr/bin/sqv+Malformed MPIcondition andSTOKE_ALLOW_UNVERIFIED_APT=1.The remaining unticked box is #23, and it is a real gap:
repoatfb5cb474still offers list / clone / create / import / rename / import-batch / transfer and nosync. The other unticked criterion — no hand-rolledcurlfor an org repo, a label, or a release asset — also stays unticked and was re-measured, not assumed:release.ymlL47-55 still attaches the.debwithcurl -F attachment=@….Rest of the board, swept this tick. #33 still real (12
kimi-reviewer-andresmgslhits inREADME.mdatfb5cb474); #36 still real (label id 261 still reads0.6.1's "owes normalization or conversion to a discussion" — thebootstrap=yesdispatch it waits on has deliberately not been fired); #32 claimed and moving with !40 open. Zeroblocked,needs-triage,attention,needs-ruling,post-mergeandoffsiteacross the whole repo, open and closed.One piece of residue cleared beyond this epic. #24 and #25 were still carrying
claimed— both auto-closed on aCloses #N, which closes an issue but never releases its claim, andissueflow-reconcileenumeratesissues?state=open, so no sweep will ever look at them again. Released on both, and on #1. Their assignees stay as build attribution.The hold on Finding 4's conversion is discharged — @andres, the only open part is when
Backlog-hygiene tick, 2026-09-01T00:25Z. Board was clean on every mechanical lever (no
blocked, noclaimed, no open PR, no obsolete issue, full 18-issue + 26-PR label audit atstate=all→ zero contradictions), so the tick went at the one hold this epic has carriedsince 2026-08-30: "how it authenticates there is undecided ... the shape is probably small,
but 'probably' is exactly what an issue may not carry."
That hold rested on a question this epic said it could not answer from the repo. It can,
and TRIAGE.md is explicit that a lifted hold makes its body prose stale in the same instant
and the body is triage's to correct in the same tick — so Finding 4 has been rewritten in
place, both halves, keeping the old wording quoted so the reason it existed stays legible.
No box was ticked and no label moved: acceptance criterion 2 is still unticked and still
true —
release.ymlL47/49/53 hand-roll threecurls withsecrets.RELEASE_TOKENat9586d2c, re-read at that SHA rather than taken from the last tick.Measured by running the CLI, not reading it (
9586d2c, stdin closed):auth loginguards both prompts on!token(src/cli.js:if (!username && !token),if (!password && !token)), and the global--config <path>flag is apreActionhooksetting
STOKE_CONFIG_FILE(L24-26), so CI never touches$HOME. No source change isneeded.
release view/create/uploadcover the whole hand-rolled block, andrelease upload --tagresolves the tag to the id itself (getReleaseByTag, L1203), so theRELEASE_IDplumbing at L47-52 disappears rather than being ported.
Three things the rewrite records that a converter would otherwise trip on:
release.ymlL35 already setsSTOKE_TOKEN— one step above thecurls — and thatreads like the auth question is already solved in this file. It is not.
STOKE_TOKENishonoured by exactly one line in the tree,
scripts/publish-deb.sh:30, and nothing undersrc/reads it:src/config.jsresolves the token only from a config file. Export itand call the CLI and you get an unauthenticated client.
node src/cli.jsavailable with no install" was false —cli.jsL3 requires
commanderand a bare checkout exits 1 withMODULE_NOT_FOUND. Harmlesshere only because L28 runs
npm cifirst, but it was the clause carrying "probablysmall", so it is corrected rather than dropped.
release create --body-file <path>exists (L1147-1148 /
readBodyOptionL62-71), so sending release notes never depended onthe conversion question. Only extraction of the 1.4.0 section from
CHANGELOG.mdremains, and it is independent. #32's matching paragraph has been narrowed the same way, so
the two do not drift; its conclusion and criteria are unchanged.
What I did not do, and why it is yours. I have not minted the conversion issue. The spec
gap is gone, so the only remaining question is sequencing, and it is not triage's: converting
this file changes the path that publishes stoke's own artifacts, and
v1.4.0is not taggedyet — #32's post-merge criteria 1-2 wake on exactly that tag push. Rewriting the publisher
while a release is pending is your call.
v1.4.0ships. Reversible, and itkeeps the release path frozen under the tag #32 is waiting on.
readyissue, accepting thatv1.4.0publishesthrough freshly-changed code.
Nothing is blocked either way, so I have not set
needs-ruling— the board would be claiminga gate that does not exist. One word here and it gets minted.
Amending yesterday's ask — @andres, the choice is wider and cheaper than I put it
Backlog-hygiene tick, 2026-09-01T01:30Z. Board clean for the fourth tick running (3 open
issues, 0 open PRs, every queue lever
0atstate=all, 18-issue + 26-PR label audit withzero contradictions). This is not new work on the board — it is a correction to the question
I left in your court at 00:26Z, which was framed on a premise I had not measured.
I asked "convert
release.ymlnow, or afterv1.4.0?" That assumed one route. There aretwo, and the tree already names the second one.
curls to stoke's own CLI. Settled yesterday; needs no sourcechange.
heavy-duty/crewandheavy-duty/provider-seeker, the org's only other release-shipping repos, hand-roll nothing:their
release.ymlis four lines ofuses: heavy-duty/ceremony/.github/workflows/release.yml@<tag>(crew0.6.2,provider-seeker
0.6.3). That workflow has zerocurlcalls, detects this forge fromGITHUB_API_URL's/api/v1shape vialib/forge.sh→forge_select, and runs on theambient
github.tokeninstead of a pastedsecrets.RELEASE_TOKEN. stoke's own.ceremony/README.mdL9-14 already records this route — and a cost of not taking it:"
docs-sync --fixcannot run until or unless a future issue adopts the ceremonyrelease-workflow pin."
Route B also closes the blank-release-page defect for free, and I was wrong that anything
was still missing there. Yesterday I said only extraction of the
## 1.4.0sectionremained. I never looked upstream for it. Ceremony ships
bin/changelog-section <version> [file]. Fetched at0.6.3fromforgejo.heavyduty.buildersand run against this repo'sCHANGELOG.mdat9586d2cunmodified:changelog-section 1.4.0→ exit 0, 21 lines, thecomplete
### Added/### Changed/### Fixedbody, empty stderr. Ceremony's release workflowpublishes that section as the release body and refuses to publish a release whose section is
missing — so under B the extraction work is nothing at all, and under A it is a
vendor-or-reimplement of ~30 lines feeding
release create --body-file.Route B's price, named rather than left to be discovered. Ceremony's workflow builds
nothing itself — it invokes the consumer's own
.github/actions/release-artifact/action.ymlafter the tag and before the publish. crew and provider-seeker both carry that action;
stoke does not (404 on stoke, 200 on both siblings), so adopting the pin means authoring
one that runs
scripts/build-deb.sh. It also does not retireRELEASE_TOKENfrom this file:scripts/publish-deb.shPUTs the.debto/api/packages/heavy-duty/debian/pool/…, whichneither route touches.
Separately, this epic's acceptance criterion 2 said "no workflow in this org" and nobody had
ever measured "this org". Now measured — 91 files across all 9 repositories of
heavy-dutyon
forgejo.heavyduty.builders(workflows, composite actions,scripts/bin/cli/lib),matching
curlwith a four-line window because a line-anchored grep missed one. Four filescarry a token-bearing
curl; exactly one does any of the three operations the criterionnames, and it is
.forgejo/workflows/release.yml. The criterion is bounded by that singlefile — fixing it ticks the box org-wide, and nothing else is owed. The other three are
enumerated in the body so a converter does not sweep them in. (Host note, the same trap as
heavy-duty/ceremonyon #36:heavy-dutyis itself two organizations — 9 repos here, 28 ongithub.com, five shared names — andstokeexists on this forge only.)What I am asking, unchanged in kind and narrower in fact: which route, and when. The
recorded default is still route A after
v1.4.0ships. Nothing on the board is gated on youranswer — which is why I have again not set
needs-ruling;v1.4.0can be tagged today againstthe release path exactly as it stands, and #32's criteria are unaffected either way. Say the
word and I mint it as a
readyissue with the route fixed.Bodies of this issue and #32 corrected in place per TRIAGE.md — a lifted hold makes its body
prose stale in the same instant, and the body is mine. I did not mint anything, tick any box,
or touch #36.
The ruling flag on this item was set by @claude-bot-andresmgsl with no accompanying
escalation comment. Setting it requires the escalation contract — the
question, the options, and a recommendation — posted by the
flag-setter no more than 15 minutes before applying the label, or any time
after (LABELS.md
carries the flag-setter's obligations; heavy-duty/ceremony#50 D4). The label stays — this machine never removes an
escalation on the strength of a timestamp heuristic — but the contract is
still owed.
🧭 needs-ruling —
.forgejo/workflows/release.yml: port its three hand-rolledcurls to stoke's own CLI, or replace the file with ceremony's reusable release workflow?Options: A — port the
curls tostoke release view/create/uploadand vendor aCHANGELOG.mdsection extractor B — replace the body withuses: heavy-duty/ceremony/.github/workflows/release.yml@0.6.3and author therelease-artifacthook stoke lacksRecommend: B, because it deletes the hand-rolled block outright rather than re-expressing it, and closes the empty-notes defect and the
docs-sync --fixblock as side effects instead of as extra work.Blocked: minting the conversion issue stops until this is answered. Everything else continues — nothing else on stoke's board waits on it, #36 is independent, and
v1.4.0has already shipped.Default: none — hard block. This decides the path that publishes stoke's artifacts, which LABELS.md makes a hard block by construction (#50 D13).
Analysis
Why this is being flagged now, having sat in prose since 2026-08-30. Finding 4
ended with "the default this epic assumes is after
v1.4.0ships; say otherwiseand it gets minted as a fresh
readyissue." That was a sequencing call and aroute call, and only the sequencing half was ever conditional.
v1.4.0shippedtoday — tagged 08:12:57Z at
523a4558,apt-cache policy stokenow offers1.4.0, #32 closed — so the sequencing half hasanswered itself and the route call is all that is left. It has been a live ask for
three triage ticks with no flag on the board, which is the board bug this comment
repairs: TRIAGE.md
says to set
needs-ruling"so the board shows where the human's turn is", and anescalation that lives only in a paragraph does not.
The new evidence, and what it does and does not decide. Run
735 — the first
time
release.ymlhas ever fired — went red. Read from the job log(
/heavy-duty/stoke/actions/runs/735/jobs/0/logs, 200; the/api/v1routes 404):tests 117/117,
.debbuilt, then Publish to Debian registry exited 1 atpublish-deb.sh:33with
error: no token, becausesecrets.RELEASE_TOKENexpanded to empty. TheCreate release and attach .deb step — the three
curls this finding is about — neverran.
v1.4.0was published by hand.That failure is deliberately not an argument for either option. This finding
already measured, on 2026-08-30, that
scripts/publish-deb.shPUTs to/api/packages/heavy-duty/debian/pool/…, a package-registry surface neither routetouches. So the absent secret bites identically under A and under B, and fixing it is
an operator act under both. It is listed below as a separate ask, not as a tiebreak.
Option A — port to the CLI. Everything it needs is measured and shipped:
release view --tag --jsonreplaces the L47 lookup,release createreplaces L49-51and prints the id,
release upload --tag --assetreplaces L53-55 and resolves the tagitself, and
auth login --token-file+ the global--configauthenticatenon-interactively with no source change (run end to end at
9586d2c). Its remainingcost is the extractor:
release create --body-filereads a whole file verbatim, sosomeone must vendor or reimplement ceremony's
bin/changelog-section(~30 lines) toselect the
## 1.4.0section. The trap recorded above still applies —STOKE_TOKENishonoured by exactly one line in the tree and nothing under
src/reads it.Option B — adopt the reusable workflow.
heavy-duty/crewandheavy-duty/provider-seeker, the org's only other release-shipping repositories, bothcarry a four-line
uses:and hand-roll nothing. Its cost is named and real: ceremony'sworkflow builds nothing itself, so stoke must author
.github/actions/release-artifact/action.yml(404 on stoke, 200 on both siblings)wrapping
scripts/build-deb.sh. What it buys beyond A: the extractor becomesnothing at all — that workflow publishes the version's own
CHANGELOG.mdsectionas the release body and refuses to publish a release whose section is missing — and
.ceremony/README.mdL9-14 records thatdocs-sync --fix"cannot run until or unlessa future issue adopts the ceremony release-workflow pin".
The recommendation, stated as a trade rather than a preference. A is the smaller
diff and keeps the workflow stoke's own. B is the larger diff and retires a file
instead of maintaining it, and it is the only option under which this epic's
acceptance criterion 2 — no workflow in this repository hand-rolls a
curl— becomestrue by construction rather than by vigilance. Recommending B; A is a defensible call
and this is reversible before any merge either way.
The separate operator ask, which is not part of this ruling.
RELEASE_TOKENonthis repository (or the
heavy-dutyorg) is unset or empty. No agent here can read orset it —
GET /actions/secretsreturns 403user should be the owner of the repo— sothis needs @andres regardless of A or B. Note the log evidence narrows what to check:
because
publish-deb.shaborted before issuing any request, the token's scopes areuntested, not failed; the question is whether it exists at all. Until it does, every
future
v*tag repeats run 735 — builds a.deb, publishes nothing, and leaves a tagwhose release page is empty unless someone finishes it by hand.
Decider: @andres. Nothing is minted until the route is picked; the ladder runs from
this comment's
labeledevent.Every issue referenced by this epic's task list is closed. Please close the epic or extend its task list.
Triage, 2026-09-01T08:49Z — answering both machine flags on this issue. The epic is extended, not closed, and the ruling contract was already met.
1.
ceremony:needs-ruling-bare(comment 32280) — the contract is satisfied; nothing further is owedThe flag is a 48-second race, and the contract's own wording covers it. Measured:
needs-rulingapplied (labeledevent, this issue's timeline)Default: none — hard block)LABELS.md requires the ask "no more than 15 minutes before applying the label, or any time after" — 32285 is 48 s after, i.e. squarely inside the permitted branch. The machine agrees on re-read: run 741's
issueflowoutput is#27: completed epic nudgedand nothing else — no second bare flag. Comment 32280 stands as a historical record of the gap, not as a live ask, andensure_comment's marker means it will not repeat.The ordering was mine and it is the avoidable half: post the escalation comment first, then apply the label. The 15-minute tolerance is one-sided for a reason — the sweep can fire in the seconds between the two writes, and it did.
2.
issueflow:epic-complete(comment 32298) — extend. This epic is not doneThe nudge is correct about what it measured and does not measure the thing that keeps this epic open.
What it measured: all six issue references under
## Task listare closed — #26, #24, #25, #23, #1, and #32. #32 is not a row of its own; the sweep reaches it through the prose inside the #1 row, becauseepic_referencesprints whole matched rows and hands every#Nin them toreference_states. That is why the nudge fired at 08:36:39Z and never before: #32 closed at 08:36:10Z, 29 seconds earlier, and until then this epic had an OPEN reference and readKEEP.What keeps it open: acceptance criterion 2 — "no workflow in this org needs a hand-rolled
curlwith a pasted token" — is unticked and honestly so. It is bounded to exactly one file,.forgejo/workflows/release.ymlL47/49/53, per the org-wide scan recorded in the criterion itself.Why there is no sixth row yet, and this is the doctrine point: that remaining work is the open
needs-rulingon this very issue (32285) — route A (port the threecurls to stoke's own CLI) vs route B (replace the file with ceremony's reusable release workflow). TRIAGE.md outcome 2 forbids minting an issue that carries an open question forward, and the two routes produce different deliverables, so a task-list row cannot exist before the route does. Adding a placeholder row would put a reference to a non-existent issue on the board, which is the lie this epic's task list is supposed to prevent.The wake, so this comment is a commitment and not a deferral — the marker suppresses any further nudge, so nothing else will ask again:
## Task listrow, and clearsneeds-rulingin the comment that records the ruling; orlabeledevent at 08:35:16Z) passes with the ask unanswered → per ceremony#50 D13–D14 the pick becomes triage's, is recorded as a decision with its reasons, and the same mint follows. The operator may overturn it at merge.This epic closes when AC 2 ticks — that is, when
release.ymlno longer hand-rolls the asset attach — and not before.Amending the ruling ask — I recommended B before reading the workflow it names. Having now read it, the recommendation flips to A
@andres — the question and the two options in comment 32285 are unchanged,
needs-rulingstays on, and the 24 h ladder still runs from thelabeledevent at 08:35:16Z, not from this comment. What changes is the recommendation and the cost basis under it, because I recommended B on the strength of its name rather than its contract. Measured 2026-09-01T08:52Z againstheavy-duty/ceremonyon this forge at tag0.6.3(8f0ef796),.github/workflows/release.yml, 385 lines, byte-identical to the sibling checkout — so this is the tree stoke'suses:would actually resolve, not GitHub's same-named repo.Recommend: A — port the three
curls to stoke's own CLI and vendor aCHANGELOG.mdsection extractor.What B actually costs, in the order I found it
if [ "$GITHUB_REF_NAME" != "$ver" ]→ "tag '…' does not match the tree's version '…' — creating nothing." The scheme is bareX.Y.Z, deliberately (L26: "no 'v' prefix — box's 0.6.0 set the scheme"). stoke shipsv1.3.0andv1.4.0, and.forgejo/workflows/release.ymltriggers ontags: ['v*']. Under B the next tag must be1.5.0, or the door fails loudly and publishes nothing. The blast radius is small but not zero: two published releases already carry thev, three README examples pass--tag v1.x.y, and the.debversion (1.4.0, never prefixed) is unaffected, so apt does not care. This is a scheme decision, which is yours, not a plumbing detail.mainand expects the family ceremony: arelease: X.Y.ZPR carrying thereleaselabel, bumpingX.Y.Z-dev→X.Y.Z. stoke'smaincarries a bare1.4.0with no-devand has no such label in use. It can be narrowed to the tag door alone by omittingbranches: [main]from the caller'son:— but then B is "adopt one door of a two-door workflow", and adopting it properly is a larger change than this criterion asks for.curls. A caller job that saysuses:has no step list, and ceremony's workflow declares exactly one input (version-source) and nosecrets:passthrough; the artifact hook is invoked withversionalone (L366-372). A composite action cannot readsecrets— soscripts/publish-deb.shand itsRELEASE_TOKENcannot live in the hook and must stay in a second job in stoke's own caller. Consistent with what the original ask already said: the registry token is route-independent..debbuild out ofnode:22-bookworm. Ceremony's jobs hardcoderuns-on: ubuntu-latestwith nocontainer:; stoke builds in a node container today.ubuntu-latestis proven on this instance (every labels/sweep run uses it), butbuild-deb.shneedingdpkg-deb+ node on that image is unverified, and it would be the builder's first spike under B.What survives from the original argument for B
Real and worth stating: under B the release create/attach runs on
GH_TOKEN: ${{ github.token }}—forge_release_createresolvesGH_TOKEN→GITHUB_TOKEN→FORGEJO_TOKEN— so the release half stops depending on the missingRELEASE_TOKENentirely (unproven for releases on this instance, proven for the issue/label writes the sweep makes). B also fixes the empty-notes defect for free: notes come fromchangelog_section, and stoke's## 1.4.0 — 2026-08-31heading parses correctly underchangelog_section_problem(/^## / && $2 == ver), so no reformatting is owed.Why A now wins on the evidence
A is bounded to one file and one repo, keeps the
vscheme, keeps the container, adopts no ceremony stoke does not practise, and dogfoods the three commands this project shipped for exactly this purpose (release view/create/upload, since033a40c). It ticks acceptance criterion 2 as written — that criterion asks that no workflow need a hand-rolledcurlwith a pasted token, not that stoke stop making HTTP calls. Its one genuine cost is the changelog extractor A must vendor, which B would have supplied.B remains the right answer if and only if you want stoke on the family's release ceremony — bare tags,
-devonmain, release-PR-as-ship-decision. That is a strategic call worth making deliberately, and if it is yours, it should be minted as its own piece of work rather than smuggled in under a three-curlconversion.Blocked: unchanged — minting the conversion issue waits on this. Default: none — hard block (published artifacts, ceremony#50 D13). If this is still unanswered at 2026-09-02T08:35Z the pick becomes triage's under D13–D14, it will be A on the reasoning above, and you may overturn it at merge.
claude-bot-andresmgsl referenced this issue2026-09-01 15:15:29 +00:00
@claude-bot-andresmgsl — this ruling is 12 hours past its
labeledevent: the ladder's12h rung (BUILDER.md — the ruling ask,
heavy-duty/ceremony#50 D13). Mechanically read, the escalation carries
Default: none— a hard block; no default ever fires.The rung's duty is the flag-setter's: re-read the
Default:againsteverything that has landed since the flag went up — does it still hold, and
has reasonable doubt appeared? A stale default does not fire, and new doubt
makes it a hard block. The rungs run on the
labeledclock and do notreset on activity; this comment fires once per flag episode.
The 12h rung, answered by the setter — the
Default:still readsnone — hard block, and the re-read found one new cost, all of it on B's sideThe machine's rung fired at 2026-09-01T21:00:26Z (sweep run 880,
schedule, started21:00:07Z,
success, 0::error/ 0::warning, logged asissueflow: #27: ruling at the 12h rung — commented (the setter re-reads the default)). Theladder is anchored to the
needs-rulinglabeledevent at 2026-09-01T08:35:16Z — threelabel events on this issue ever (
epic,scope:cli,needs-ruling), no re-flag, so this is thefirst and only episode. The rung's duty is mine as the setter: re-read the
Default:against whathas landed, and where doubt has appeared, make it a hard block.
1. What landed since the flag went up, and what it touches. Four stoke commits reached
mainafter 08:35:16Z —
3ea8eb90(CONTRIBUTING),112f9468+d84062af(.github/labeler.yml,.github/labels.conf,scripts/check-governance.js,test/governance.test.js), andda43f967,merged as !52 at 19:14:51Z (
967efa02: the three.github/ISSUE_TEMPLATE/forms, CONTRIBUTING,changelog.d/50.md). None of them touches this question's surfaces —.forgejo/workflows/release.yml,scripts/publish-deb.sh,scripts/build-deb.sh,src/cli.jsandCHANGELOG.mdare all untouched in that range. On the other side of the pin, ceremony'smainlastmoved at 2026-09-01T05:52:26Z (
91aee7f8), which is before the flag, and its tag list isstill
x-total-count: 8with0.6.3(8f0ef796) on top. #36's two newest defects (9 and 10)were measured in that window but live in
lib/issueflow.sh'spost_merge_decision, a path thisboard already runs hourly and one that ceremony's release workflow never enters — no route-specific
doubt from them.
2. The
Default:is not stale, because there is nothing to go stale. It readsnone — hard block(published artifacts, ceremony#50 D12–D13). A hard block cannot fire earlyand cannot expire; the 0–12h rung's "proceed when a still-clear, reversible default expires" never
applied here. Nothing about the last twelve hours changes that, and the block stands.
3. Doubt did appear — and it lands entirely on B, so it deepens the recommendation rather than
disturbing it. Comment 32351 measured route B against
0.6.3and listed four costs. A fifth wasnot in that list: at the only tag stoke could pin today, B's publish is not atomic, and the
hand-rolled block B would delete is.
.github/workflows/release.yml@0.6.3L385 callsforge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}"with no existence checkon the tag door, and that function at
0.6.3POSTs the release withdraft:falsefirst anduploads assets after — an upload failure returns 1 with the release already published, no rollback,
and no get-or-create, so a job re-run meets Forgejo's conflict refusal instead of finishing the
attach. stoke's own three
curls are better on that one axis:release.ymlL47-52 is get-or-createand
scripts/publish-deb.shL42-51 treats 409 asAlready published, so a re-run recovers.Upstream fixed it and the fix is in no tag:
heavy-duty/ceremony#271— "publish the releaseatomically, so a failed asset upload cannot strand a published, incomplete release" — merged as
!272 (
58ab5036) at 2026-08-30T10:47:55Z, rewriting the function to create a draft, upload,then publish, with rollback and stranded-draft cleanup.
0.6.3was cut 2026-08-26T20:18:16Z andgit merge-base --is-ancestor 58ab503 0.6.3is false — the fix lives only on ceremonymain(
VERSION=0.6.4-dev). All of it measured againstforgejo.heavyduty.builders, not GitHub'ssame-named repository, which is the tree a
uses:would resolve.Why it had not been recorded: triage examined ceremony#271 on 2026-08-30 and filed it as not a
defect for stoke, because stoke pins only the label and issue-flow callers and never reaches
forge_release_create. That is still true of the pins stoke carries today — and route B isexactly the change that makes stoke reach that function. A ruling scoped to the current
configuration does not survive a proposal to change the configuration. This is the second half of
that ruling, not a reversal of it.
The cost is dated, not structural. A
0.6.4carrying #271 erases it, and #36's last twocriteria are already watching for that tag.
Recommendation unchanged: A (comment 32351) — port the three
curls to stoke's own CLI andvendor the changelog section extractor. Blocked: unchanged — minting the conversion issue waits
on this ruling; everything else on this epic is done. Default: unchanged —
none — hard block.@andres — the ladder's next rung is 2026-09-02T08:35:16Z, 24h from the
labeledevent, wherethe pick becomes triage's under D13–D14. It will be A, on the reasoning above plus this fifth
cost, recorded as a decision with the conversion issue minted, a sixth epic row added and
needs-rulingcleared in one comment. You can overturn it at merge.Finding 4's "Route B is not free" paragraph now carries this cost and the reason it was missed
(body edit 21:06:38Z), so the pick tomorrow reads it from the body rather than from this thread.
@claude-bot-andresmgsl — this ruling is 24 hours past its
labeledevent: the ladder's24h rung (BUILDER.md — the ruling ask,
heavy-duty/ceremony#50 D13). Mechanically read, the escalation carries
Default: none— a hard block; no default ever fires.At 24h the builder proceeds regardless, as a PR: pick an option and
state in the PR body which way you went and what doubt remains. Nothing
merges by this — the human still gates the merge. Past 24h the choice is
triage's to make: triage picks the option, records it as a decision, and
remains accountable; the operator may overturn it at merge. The rungs run on
the
labeledclock and do not reset on activity; this comment fires onceper flag episode and covers everything past 24h — there is no further
timer.
The ruling, decided by triage past the 24h rung: route A.
needs-rulingis cleared with this comment, and the work is #54The rung fired on sweep run 894 (
schedule, started 2026-09-02T09:00:05Z,success, 0::error/ 0::warning), logged asissueflow: #27: ruling at the 24h rung — commented (the builder proceeds as a PR; past 24h is triage's). The ladder is anchored to this episode'sneeds-rulinglabeledevent, 2026-09-01T08:35:16Z — re-read from the timeline before writing this, and still exactly three label events on this issue ever (epic2026-08-18T00:23:51Z,scope:cli2026-08-21T06:40:51Z,needs-ruling2026-09-01T08:35:16Z), so no re-flag and no second episode. The 24-hour mark was therefore 2026-09-02T08:35:16Z. No human ruled in the intervening 24 hours: repository-wide, the only comments since my 12h-rung answer (33454, 2026-09-01T21:07:12Z) are 33561 — my own note on #36 — and 33954, the machine's rung comment. Nothing from a human, here or anywhere on this board. Under BUILDER.md's ladder and TRIAGE.md outcome 3, past 24h the pick is triage's, recorded as a decision, and triage stays accountable for it. @andres can overturn this at merge — nothing here merges by itself.The decision: convert
.forgejo/workflows/release.yml's three hand-rolledcurls to stoke's own CLI (route A). Do not adopt ceremony's reusable release workflow (route B) today.Why A, measured this tick rather than recalled
Route B was the more attractive option on paper and the 12h rung had already found one cost the
0.6.3measurement missed. Re-firing that measurement at the 24h rung — under the standing rule that a ruling scoped to the current configuration does not survive a proposal to change the configuration — turned up two more, and both land on B.1. The fifth cost is three commits, not one.
git log 0.6.3..origin/main -- .github/workflows/release.yml lib/forge-forgejo.shagainst this forge returnsc4315c2"publish Forgejo releases atomically" (2026-08-30T09:25:43Z — the ceremony#271 fix named at the 12h rung),734676a"guard tag release publication" (2026-08-30T09:27:08Z), which adds a no published release may exist step to the tag door, and7bd331a"resume stranded merge-door publishes" (2026-08-31T11:17:49Z). All three are on the exact surface B adopts. None is in any tag: ceremony's tag list here is stillx-total-count: 8, top0.6.3(8f0ef796, 2026-08-26T20:18:16Z),mainat91aee7f8/VERSION0.6.4-dev.2. A sixth cost, and this one is structural — a
0.6.4does not erase it. Ceremony's tag door asserts the pushed tag equals the tree's version verbatim:release.yml@0.6.3L347 isif [ "$GITHUB_REF_NAME" != "$ver" ]→ "does not match the tree's version — creating nothing", exit 1. Every release tag stoke has ever pushed isv-prefixed (v1.3.0,v1.4.0), its workflow filterstags: ['v*'],package.jsonholds1.4.0, and there is noVERSIONfile. Under B,v1.5.0is refused. Adopting B means renaming the release-tag convention of a repository whose artifacts are already published under the old one — the apt registry, two release pages, the install docs. That is the published-artifacts class this epic's own escalation calls a hard block by construction, so B would not merely swap machinery; it would re-open a human-owned decision rather than close one.3. B's two known costs are unchanged.
.github/actions/release-artifact/action.ymlis still 404 on stoke and 200 on both siblings (re-fetched this tick; crew pins0.6.2, provider-seeker0.6.3), so B still means authoring one that runsscripts/build-deb.sh. And B still does not retireRELEASE_TOKEN:scripts/publish-deb.shPUTs to/api/packages/heavy-duty/debian/pool/…, which neither route touches.4. A is small, and every capability it needs is shipped and was re-run against
967efa02while writing #54 — not read, run:auth login --token-filewith stdin closed → exit 0;release view --tag v1.4.0 --json→ exit 0, release id 63;release view --tag v9.9.9→HTTP status: 404, exit 1, which is the get-or-create branch working. The notes extractor is no longer unspecced under A either: ceremony'sbin/changelog-section+lib/changelog.shat0.6.3, run unmodified against this repository'sCHANGELOG.md@967efa02, gave1.4.0→ exit 0 and 21 lines,1.5.0→ exit 1 withno section for '1.5.0'on stderr and nothing on stdout. It is six lines ofawkto vendor.5. A keeps the one thing this file is genuinely better at. L47-52 is get-or-create and
publish-deb.shtreats 409 asAlready published, so a re-run recovers. At0.6.3B's door does not, and that is the axis the 12h rung was about.One measurement that dissolved a worry, recorded so it is not re-raised
Adopting the pin would not have forced stoke onto ceremony's merge door. Both doors live in the reusable workflow and the callers keep only triggers and permissions, so a tag-driven stoke stays tag-driven under B. B's problem is not the door it would use; it is what that door does at the only tag stoke could pin.
The doubt that remains, stated plainly
0.6.4carryingc4315c2+734676a+7bd331aerases the atomicity and guard costs entirely. B is still the better long-run shape — one four-line caller instead of a bespoke workflow, notes for free, anddocs-sync --fixunblocked (.ceremony/README.mdL9-14). Its wake condition is already on the board: it is #36's re-pin, which waits on the same0.6.4. Cost 6 — the tag rename — survives that bump and is a separate human decision whenever B is next raised; I am not deciding it here and #54 does not touch the tag convention.RELEASE_TOKENin this file. The conversion removes the hand-rolled HTTP, which is what acceptance criterion 2 asks for; it does not remove the pasted secret. Whether the ambient${{ github.token }}could replace it is a real question and I have deliberately made it an explicit non-goal of #54 rather than letting a refactor change this file's credential surface under cover.scripts/publish-deb.sh:33witherror: no tokenbecausesecrets.RELEASE_TOKENexpanded to empty, andrelease.ymlis byte-identical atv1.4.0andmain, sov1.5.0fails the same way under either route until it is set. That step also runs before the release step, which is why #54's post-merge criterion names that tag push as its wake condition and says so honestly instead of pretending the merge proves it.What this comment does, in one place
enhancement+scope:packaging+ready, unclaimed, created 2026-09-02T09:14:16Z. Route A as a decided spec: two new scripts (scripts/publish-release.sh,scripts/changelog-section.sh), two new tests, one workflow step reduced to a line, and the empty-notes defect closed as a side effect.08:40Z: thelabeledevent is 08:35:16Z, and that is what the ladder anchors to.needs-rulingand returns this epic to its flow. It stays open as anepic, now waiting on #54 rather than on a human, and closes when acceptance criterion 2 ticks.attention. This ruling hands nobody a claim: #54 is unassigned and #27 is unassigned, and flagging an unassigned issue is a board bug rather than a demand.A verifiable prediction for the next sweep, since this comment claims the board changed: run 894 logged
issueflow: #27: completed epic nudged, on the reading that every child in the task list was closed. With #54 open in that list,epic_decisionnow sees anOPENstate and returnsKEEP, so that nudge should stop from the next run onward. If it does not, the row is not being read and that is a #36 defect, not a board fact.Epic de-rot: two sentences in this body asserted #54 was
readyand unclaimed. It has beenclaimedsince 2026-09-02T09:17:59Z. Body corrected at 10:30:23Z; nothing else on the board changed.Both sentences were written at 09:16:22Z and were true then. They went stale 97 seconds later and stayed that way for a tick, because the check I ran last tick read the task list's checkbox structure (6 rows, 5
[x], one[ ]) and the sweep's own silence, and neither of those can see prose inside a row.epic_referenceshands each row to a state check that only asks OPEN/CLOSED — a row may describe a child's queue label completely wrongly and still produce a correct nudge decision. The machine cannot audit this class of rot; only triage can.Measured from #54's label events, not from its thread (TRIAGE.md's rule before asserting label-borne state in prose):
enhancement,ready,scope:packagingaddedreadyremovedclaimedadded, self-assignedRefs #54#54's live label set is
claimed+enhancement+scope:packaging, assignee @codex-bot-andresmgsl.What changed in the body: the "It is minted …
readyand unclaimed" sentence now records the claim and the PR; the sixth task-list row now reads**Claimed by @codex-bot-andresmgsl 2026-09-02T09:17:59Z** and built as !55 … Live labelsclaimed+enhancement+scope:packaging.The checkbox stays[ ]— #54 is open, and its close is triage's under its own post-merge criterion, not the merge's.What did not change, verified rather than assumed: replaying
epic_references(issueflow-reconcile.sh+lib/issue_references.shat the pinned0.6.3) over the patched body returns the same seven locals —#1 #23 #24 #25 #26 #32 #54— and the same 6 rows / 5[x]/ one[ ]. !55 is linked by URL and never as#55, so no new child was injected; #54 is still OPEN, soepic_decisionstaysKEEPand the completed-epic nudge stays silent for the reason it has been silent since 09:16Z.No
attention: this epic is unassigned, and flagging an unassigned issue is a board bug rather than a demand. Nothing is owed by anyone on this edit.Acceptance criterion 2 is ticked, and the body is re-aligned to the board. !55 merged into
mainat 2026-09-02T11:22:58Z asd6a21c9d, converting.forgejo/workflows/release.yml— the one file this criterion was measured down to.Three anchored replacements, each asserted to match exactly once before the PATCH (39157 → 41165 bytes):
release.ymlL47-55 still hand-rolls the asset attach, so this stays unticked" replaced — it went stale the instant the merge landed.claimedoff, assignee released,post-mergeon). It stays[ ]— #54 is open.The tick was re-measured, not inherited. The 2026-09-01 org scan is a day old, so rather than trust it triage checked which of the 9 forge repositories have moved: only
ceremony,office-equipmentandstokehave default-branch commits on or after 2026-09-01, and those three were re-scanned in full at their live heads (same method — workflow/composite-action YAML plusscripts/bin/cli/lib,curlmatched with a four-line window).stoke@d6a21c9d: one hit,scripts/publish-deb.shL38, the out-of-scope registryPUT—release.ymlis gone from the results entirely.office-equipment@82e32a9c: none.ceremony@91aee7f8: the same two out-of-scope files, with line numbers drifted (lib/forge-forgejo.shnow L143/160/532/536/664/855 against L143/160/532/664/851 recorded here) — which is itself the evidence the scan was re-run rather than copied.Criterion 5 of #54 was verified against the merged tree, not the PR's worklog, along with its other four pre-merge criteria and all six tasks; the measurements are on #54.
Why this epic is not being closed, though all four of its criteria now read
[x]. Its last child #54 is open atpost-merge, holding one operator-gated criterion: @andres setsRELEASE_TOKEN, then the firstv*tag push proves the release step. An epic is its children's progress view, so closing it now would retire the view while the last child is still in flight. The close condition is therefore #54's close, and triage owns both. This is the reversible call — if the operator would rather see #27 closed now on its criteria, with #54 carrying the residue alone exactly as #1 carried its residue to #32, say so and it closes in the next tick.The sweep's reading of this body is unchanged, verified by replaying
epic_references(issueflow-reconcile.sh@0.6.3L400-407) over the patched text: the same seven locals1 23 24 25 26 32 54, and the same 6 rows / 5 checked / 1 unchecked. !55 is still linked by URL only, so no#55child was injected, and neitherd6a21c9dnoree88d7d3can parse as a reference.🔧 One paragraph corrected — it asserted a queue state the board had already contradicted. No task-list row changed, no criterion changed, no label change; this epic stays open on #54.
The paragraph below the task list, written at 19:50Z, said the
1.5.0tag "has a vehicle as of this tick — #56, release1.5.0, mintedreadyand unassigned". @codex-bot-andresmgsl claimed #56 four minutes later, at 19:52:31Z, and its PR !58 has since merged intomainas088e7e2d(2026-09-02T20:18:58Z). The paragraph now records what is actually true: the vehicle has landed, #56's five pre-merge criteria are verified at that commit, the sweep derived its transition at 20:47:58Z, and #56 readspost-mergeand unassigned with only its three tag-gated criteria left.This is the rot class this epic has been bitten by before — body prose asserting a label state, which goes stale the instant the board moves and which no same-issue audit catches, because the checkbox next to it was never wrong. The row for #54 above was re-read line by line and is accurate as written.
What this epic closes on has not changed. #56 is still not a child of it — the scope is the five CLI gaps, not shipping, the same reading that kept #32 out — and it earns no row. #54 is still the last open child, still
post-merge, still holding one operator-gated criterion whose wake is thev1.5.0tag push at088e7e2d. Tags are still 2. When that tag lands and #54 closes, this epic closes with it; comment 34121's standing offer to @andres to close it earlier is still unanswered and still his to take.🔧 One measured clause went stale on the same commit as #54's — corrected. No task-list row changed, no criterion changed, no label change; body only.
What was false. The route-B cost analysis read "
package.jsonholds1.4.0". At088e7e2dit holds1.5.0— #56's bump, merged 20:18:58Z. Verified byGET /contents/package.json?ref=088e7e2d→ 200,version=1.5.0.What survives untouched, and why the correction is small. The argument does not rest on the number. Ceremony's tag door asserts
GITHUB_REF_NAME == version_read; every release tag stoke has pushed isv-prefixed, sov1.5.0is refused by that assert whether the tree says1.4.0or1.5.0. The sixth cost is structural, exactly as the paragraph claims. Only its supporting measurement had drifted.The
VERSIONhalf re-verified rather than assumed.GET /contents/VERSION?ref=088e7e2d→ 404, run against a 200 control onpackage.jsonat the same ref — a 404 with no control is not evidence of absence on this instance. Still noVERSIONfile; that clause stands, and now says so with its measurement attached.Epic audit this tick, row by row including prose. 9
[x]/ 1[ ]. The unchecked row is #54, whose prose still reads correctly: open, one surviving post-merge operator-gated criterion, triage owns the close. #56 still earns no row — this epic's scope is the five CLI gaps, not shipping (the same reading that kept #32 out). The paragraph naming #56's post-merge state, corrected yesterday at 20:49Z, re-checked against the board today and still accurate:post-merge, unassigned, three tag-gated criteria left.This epic closes when #54 closes, and #54 closes on one tag push. Nothing here changes that.
Triage — epic currency: the tag fired, and this epic's progress view had not moved with it.
Body patched just now (43824 → 45155 B, two asserted replacements). Both clauses were true when
written and were inverted by one event — the
v1.5.0push at088e7e2d, 2026-09-02T22:29:15Z:v*tag" — the tag came. #54's criterion still does notread, so the sentence now points at a door that has already been walked through.
post-merge criterion is ticked (the tag resolves to
088e7e2dandrelease.ymlran).Re-measured this tick, not carried over:
GET /repos/heavy-duty/stoke/tagsv1.5.0→088e7e2d,v1.4.0,v1.3.0— 3deb,pushonv1.5.0)status: failure, started 22:29:31ZGET /api/v1/packages/heavy-duty?type=debiandebian stoke 1.5.0, created 22:29:42ZGET /repos/heavy-duty/stoke/releases/tags/v1.5.0v1.5.0So the release is half-shipped: the registry step returned
201and the Create release and attach.debstep died on403 … required scope(s): [read:user]. The old prediction is kept in the bodyrather than overwritten — it is the record of what the board was told, and the correction is the
finding.
Wake condition for this epic is now the re-run of run 990's job, not another tag. #54's AC6 and
#56's two remaining criteria all key on that same re-run, so nothing here moves before it does.
No label flip is owed: the epic keeps
epic+scope:cli, #54 keepspost-merge, and the task listabove is otherwise accurate — #54 remains this epic's only open child and the only row that stays
unchecked.
Triage tick — the Context tally said
leaving two; all five have been closed since 2026-08-31Board otherwise static: 6 open issues / 2 open PRs, 28 labels, tags 3
(
v1.5.0/v1.4.0/v1.3.0),main088e7e2d. Newest repo comment before this one was my own34435(04:56:20Z). Every open item readsupdated_at2026-09-03T04:56:20Z— that iscross-reference fan-out from
34435, not movement.The find —
## ContextL4, corrected in place (body45156→46351B)The opening paragraph read:
Measured from the forge this tick, not from the thread:
stateclosed_atfb5cb474, merged 16:05:04Zc34a8b04, merged 18:42:40Zleaving twoisleaving zero, and has been for three days. The sentence is kept verbatim andannotated in place — the rot is the finding. Read at face value it sends a reader hunting for two
open children of this epic that do not exist; the only open child is #54, created
2026-09-02T09:14:16Z, which is not one of the five.
It also contradicted this body's own Task list, ~430 lines below, where all five rows are
[x]andthe summary line already says "Every issue this list references except #54 is closed." Two claims
in one body, one current and one stale — the tick-13 lesson (correct the record that owns the
fact, not only the one where it happened) turns out to have an intra-body form.
Why five sweeps walked past it — a third rot shape, after nouns and imperatives
Tick 94's find was a spent imperative in a checkbox-free section, and its rule was "sweep for
imperatives, not for the last finding's nouns." This one is neither. It is a running tally
over board state, in the indicative, carrying none of the hunted nouns. What hid it is that it
was maintained — three times, once per closure. A tally updated three times reads as maintained,
and that is exactly what makes the fourth miss invisible; an abandoned tally is more dangerous than
a never-updated one, because its own history vouches for it. New detector, run this tick over all
eight open bodies:
leaving (one|two|…)|still open|are open|open issues|both (are|remain).Everything else it flagged on this epic is true and left alone, enumerated so the non-fixes are
on the record: L448 (
#54 … [ ]— #54 is open), L457 ("its last child #54 is still open atpost-merge" — verifiedstate=open, label present), L355's "Still not minted" (alreadykept-and-annotated with "It is minted: #54"), and L432 ("both are now buildable as specced" — a
claim about the specs, not about board state, and !38/!41 proved it right).
The four named duties — all empty, tenth tick running
blockedis 0 board-wide atstate=all. Never armed.Closes), #60 → !61 (Refs); both open,mergeable: true, both authoredby their assignee @codex-bot-andresmgsl. Never arms.
openand still ceremony's only open issue.[x]and all five verifiedclosedabove; #54[ ]and verifiedopen. #57 and #60 stay deliberately not rows: #57 is ashell-script diagnostic and #60 a workflow comment, neither is a CLI gap.
Unchanged, and still the whole board
GET /releases/tags/v1.5.0→ 404; runs withhead_branch == v1.5.0→ 1 (run 990,failure);GET /actions/secrets→ 403. One operator act is still owed and it discharges #56criterion 2, #54 AC6 and #60's post-merge criterion together: @andres mints a new PAT carrying
read:user+write:package+write:repositoryinto orgRELEASE_TOKEN, then re-runs run 990'sjob. No re-tag, no hand-created release.
Sweep 1052 (
schedule, 05:00:04Z), 255 lines, 0::error/ 0::warning, bothreconciled.,🏁 Job succeeded, and theUser 'danmt' not exist/requested danmt (round passed)pair twice — #36 defect 1 answers NO a thirteenth time. No epic-complete nudge fired,which is this epic's own prediction holding while #54 stays open. No write owed for a repeat.
Triage tick, 2026-09-03T07:0xZ — one body edit on this issue, no label or state change anywhere on the board.
The find: a universal claim whose enumeration rotted while the claim stayed true
The tag-scheme paragraph in the A-vs-B ruling reads:
The bolded claim is true. The list is short one member.
v1.5.0was pushed at088e7e2don 2026-09-02T22:29:15Z by @claude-lead-andresmgsl (annotated tag6ba87b48, messagestoke 1.5.0);GET /repos/heavy-duty/stoke/tags→x-total-count: 3. So the clause that callsv1.5.0a hypothetical — "Under B,v1.5.0would be refused" — sits one sentence after a list that omits the fact that it already exists.Why nothing caught it. Re-verifying the sentence confirms it: every tag really is
v-prefixed, three for three. A false-statement grep finds nothing to flag, and a re-check of the conclusion never descends to the evidence. This is the fourth rot shape logged here, after nouns, imperatives, and the running tally (leaving two, comment above): a universal claim with an exhaustive-looking enumeration launders its own evidence. The tally at least went false when it went stale; this one never does.It is the same intra-body contradiction as
leaving two, and against the same body. This issue already records the push twice — the## Notesgate ("v1.5.0was pushed at088e7e2don 2026-09-02T22:29:15Z, run 990 fired on it") and, on #54, acceptance criterion 6's "This repository now has three tags." #54's own earlier "exactly two tags,v1.3.0andv1.4.0" was corrected in place within the same criterion when the tag fired. This paragraph, ~125 lines up, was not — because correcting it required noticing that a true sentence had a stale list.Nothing about the ruling changes; it gets stronger. The cost this paragraph weighs — renaming the tag convention on a repository whose artifacts are already published under the old one — now spans three tags, not two.
Kept, not rewritten. The
v1.3.0,v1.4.0pair stays verbatim and the correction is appended in place, per the rule applied to #54, #56, #60 and this body's## Context.Deliberately not changed
x-total-count: 2(v1.4.0,v1.3.0).v1.5.0has a tag and a registry.debbut no release (GET /releases/tags/v1.5.0→ 404). Do not "fix" it to three.9586d2c(release list→v1.3.0) and the #1 snapshot block — anchored to a commit and self-dating, not present-tense claims.Board state — the four named duties are empty for the eleventh consecutive tick
blockedisx-total-count: 0atstate=all. Never armed on this board.open,merged=false,mergeable=true, both authored by the assignee @codex-bot-andresmgsl. Nothing to reclaim.openand still that repo's only open issue.closed, #54openatpost-merge, its row correctly unticked. #57/#60 remain deliberately non-rows.Still one operator act, unchanged
GET /releases/tags/v1.5.0→ 404; runs withhead_branch == v1.5.0→ 1 (run 990,failure);GET /actions/secrets→ 403;main088e7e2d. @andres mints a new PAT carryingread:user+write:package+write:repositoryinto the orgRELEASE_TOKEN, then re-runs run 990's job. No re-tag, no hand-created release —publish-deb.shL44 answers the repeat409withoutexit 1, so the re-run walks past the registry step to Create release and attach .deb, which has never completed. That one act discharges #56 criterion 2, #54 AC6 and #60's post-merge criterion together.Sweep 1056 (
schedule, 07:00:04Z): 255 lines, 0::error, 0::warning,labels: reconciled.+issueflow: reconciled.,🏁 Job succeeded, and no epic-complete nudge — this epic's own prediction that the nudge holds while #54 is open, confirmed a fourteenth time.Triage, 2026-09-03T08:0xZ —
## ContextL64 said "open PR #21". !21 closed unmerged on 2026-08-30. Kept verbatim, annotated in place. No task-list row changed.The sentence is in the
[trusted=yes]paragraph that explains why #1 was rescoped, and its substance is fine — that failure is real and !21 is still its clearest statement. What rotted is one adjective:#21has been closed, unmerged, since 2026-08-30T08:59:29Z, four days.What makes this one worth recording is where the contradiction already lived. This body says it twice, ~370 lines further down — L433 ("#21 was a PR, not an issue, and was out of scope here") and the #1 row ("!21 had closed unmerged and its fallback narrowing was folded into #1 rather than re-minted"). Both of those were written after the close and are correct. So the epic has carried a true record and a false adjective about the same object, in the same body, for four days — the third intra-body contradiction found here in three ticks, after
leaving two(tick 95) and thev-prefix tag list (tick 96).The class, because it caught three bodies at once today
All of today's finds are the same shape: a state word welded to a cross-reference — "open PR #21", "#57 (
claimed, open PR !59)" on #60, "Nowclaimed… with open PR !59" on #56. The claim is about another object's board state, so every check aimed at the sentence's own subject sails past it: the paragraph is about[trusted=yes], and[trusted=yes]is exactly as broken as it was. Two of the three were falsified by this morning's merges; this one had been wrong for four days and no sweep of this body had ever reached it, because nothing in it reads as a claim needing verification.No epic row moved. #57 and #60 are still deliberately not children of this epic — release-door work, not the five CLI gaps — and closing #57 does not change that. The one unticked row, #54, is still open at
post-mergeand still waits on the same operator act as #56 and #60: a newRELEASE_TOKENPAT, then a re-run of run 990's job.— triage (@claude-bot-andresmgsl)
Triage, 2026-09-03T08:1xZ —
### Finding 4's opening was superseded by !55 a day ago and a ticked criterion still points readers at it. One annotation, original text kept verbatim; no label, state or checkbox change.What was wrong. Finding 4 was recorded 2026-08-30T13:20Z, when the capability existed but this repository's own release door still hand-rolled it. !55 merged as
d6a21c9dat 2026-09-02T11:22:58Z and converted that door, and the finding's opening was never re-read afterwards. Four present-tense claims are now false:.forgejo/workflows/release.ymlL47-55 still does all of it by hand";Measured, not inferred. At
main2230ca25.forgejo/workflows/release.ymlis 43 lines and contains 0 occurrences ofcurl; the Create release and attach .deb step is one line delegating toscripts/publish-release.sh. At967efa02— the pre-conversion tree — it was 56 lines with 3curls, and L47-55 was precisely the block the finding names. So claim 3 was true when written and the citation was exact; it is the merge that spent it, and the cited line range no longer exists in the file.Why this one is worse than an ordinary stale sentence.
## Acceptance criteriabullet 2 is[x]— ticked on !55's merge — and its own annotation ends "See finding 4." A reader who follows that live pointer out of a ticked box lands on a section that tells them, four times and in the present tense, that the box cannot be ticked. The contradiction is inside one body, ~315 lines apart, and each half reads as authoritative on its own.What I did and did not do. Inserted one bold block immediately under the heading — where the pointer lands — enumerating the four claims and re-measuring them at
main. The original text is kept verbatim and nothing below the block is touched: the later paragraphs go on describing the threecurls in the present on purpose, because they are records of what was measured about them while they existed, and rewriting a landed measurement into past tense destroys the record. The finding's reasoning is untouched — it is why #54 exists.Not changed, deliberately. No checkbox moved: bullet 2 was already correctly ticked, and the epic's other criteria are unaffected. This epic stays open for the reason it already records — its last child #54 is open at
post-merge. The task list is current: #26/#24/#25/#1/#23 verified closed and ticked, #54 verified open and unticked.Also checked and deliberately left.
## SpecL343's ".forgejo/workflows/release.ymlL16-17 triggers ontags: ['v*']" is an imprecise citation, not an inverted one — the trigger block is L13-16 pre-conversion and L15-18 atmain, and the substance (av*tag trigger, unchanged across every commit in this repo's history) is true. Left as-is; recording it here so the next tick does not re-derive it.Triage, 2026-09-03 — three unpinned line citations into
scripts/publish-deb.shwere re-pointed by a merge; bodies annotated, nothing deleted.scripts/publish-deb.shhad exactly two commits in its life —f5615455(2026-07-22) and3068809b(#57 / !59) — and the second merged intomainasbbde478at 2026-09-03T07:30:12Z, ~1h40m before this tick. It replaced the one-line empty-token guard with a six-line heredoc and pushed everything below it down by 8 lines. Every citation of that file written before 07:30:12Z is therefore at the old numbering, and this body carried three of them:d6a21c9d/088e7e2d(52 lines)main2230ca25(60 lines)publish-deb.sh L42-51treats 409 asAlready publishedcaseL42-51,409arm L44 ✅caseL50-59,409arm L52publish-deb.sh L38, a PUT to /api/packages/…PUTL38-40 ✅publish-deb.sh L38 — the out-of-scope registry PUTPUTL38-40 ✅Every one of them was exact when written. Nothing was wrong then and nothing is wrong in substance now: at
main,grep -c 'Authorization: token' scripts/publish-deb.shis still 1, it is still the package-registryPUT, still out of scope, and the409arm still printsAlready published (409): …withoutexit 1. Only the numbers moved. So both originals are kept verbatim and annotated in place.The route-B citation is the one that matters, and it does not want re-pointing at
main. It backs the recovery argument — a re-run of run 990's job walks past the registry step — and a re-run replays the tree at088e7e2d, whereL42-51is exactly right. Annotated to say read it at088e7e2d, not atmain. The sibling citation one clause earlier (publish-release.shL44-49) was already pinned to that commit, which is what made the unpinned one read as amaincitation.Why AC2 is the sharper instance. That criterion's own re-measurement paragraph corrects a sibling repo's drifted line numbers — "their line numbers have drifted with the tree (
lib/forge-forgejo.shnow L143/160/532/536/664/855, recorded here on 2026-09-01 as L143/160/532/664/851)" — and then states stoke's own number, in the same sentence, in a form that has since drifted the same way. The discipline was applied outward and not to the line beside it.Re-resolved in the same pass and found unchanged, so recorded as non-findings:
src/cli.jsL154 (read:package/write:package),src/cli.jsL3 (require('commander')),src/cli.jsL225 (tokenClient.get('/user')),src/cli.jsL284/L290/L401/L570/L657 (theconfig.loginfallbacks on #60),publish-release.shL41 (stoke auth login) — all exact at2230ca25.heavy-duty/ceremonyis still at91aee7f8withlib/forge-forgejo.shcurls at L143/160/532/536/664/855, exactly as this criterion already corrected.The rule this generalizes to, for whoever writes the next citation: an unpinned
L<n>into a repo file is a claim aboutmainthat decays silently on any merge touching that file above the cited line — it does not 404, it points somewhere else and still reads as a measurement. Pin the ref, or expect to re-resolve it. And when a citation is load-bearing for a re-run, pin it to the commit the run replays, not tomain.(Board unchanged this tick: 5 open issues, 0 open PRs,
blocked0,claimed0, 28 labels all true,main2230ca25. The epic task list above was walked live — #26/#24/#25/#1/#23 closed and ticked, #54 open and unticked — and is correct. Probe 1 is still unfired:GET /releases/tags/v1.5.0→ 404, run 990 still the only run onv1.5.0and stillfailure.)Triage, 2026-09-03 — same shape, second and worse instance in this body: the capability table's
src/cli.jsnumbers, and the drifted citation that confirms itself.Following the rule in the comment above — resolve every unpinned
L<n>at the ref a reader will use — I resolved the rest of this body's citations. The capability table under "The capability is complete" is the second hit, and it fails in a nastier way than thepublish-deb.shones.The sentence above the table does name its ref ("Verified on
mainat033a40c"), but the table's own column header reads "surface onmain", and a reader today resolves it at today'smain. !41 (#23,repo sync,c34a8b04, 2026-08-31) inserted a whole command block atsrc/cli.jsL455-498 — above everything the table cites — taking the file 1742 → 1786 lines and shifting every cited line by exactly +44:033a40c)main2230ca25-o, --owneronrepo create--label <name...>--asset/--asset-namerelease uploadsrc/api.jsL277uploadReleaseAssetapi.jsis 393 lines at both refsRow 1 is the one worth keeping as a control. At
033a40c,cli.jsL458 isrepo create's.option('-o, --owner <owner>', 'repository owner (authenticated user or organization)'). Atmain, L458 is.requiredOption('-o, --owner <owner>', 'repository owner')— which belongs torepo sync, the command !41 inserted directly aboverepo create. The stale citation still lands on an-o, --ownerline. Checked the way citations are usually checked — does the flag match? — it confirms itself, and the check passes on the wrong command. A drifted citation that lands on a blank line or a brace announces itself; this one does not.So the check is not "does the cited line mention the thing" but "does the cited line sit inside the construct the sentence claims it does". Resolve the enclosing command/function, not just the token.
Claim unaffected: all three capabilities still ship at
main, so the table's conclusion and the criterion it feeds are unchanged. Only the numbers moved. Table kept verbatim and annotated in place.This body cites two different refs, which is worth knowing before trusting any number in it.
L1203(getReleaseByTag) andL1147-1148(--body-file) in the route-A section aremain-numbered and exact today — at033a40cthose same numbers are.description('Manage repository labels')and.command('upload').L24-26(the--configpreActionhook) is above every insertion point and is exact at both.readBodyOptionL62-71 andsrc/cli.jsL154 are exact atmain.Checked for the same rot on every other open body, and it is clean: #54's capability table is fully pinned to
967efa02(cli.js#L186,#L1191-L1197,#L1203), and #56/#60'spublish-deb.sh,publish-release.shandcli.jscitations are pinned to088e7e2d. #27 is the only body on the board carrying unpinned numbers, and it now carries all three annotations.Triage, 2026-09-03 — the #54 row's label enumeration is incomplete, and triage's own label mint is what falsified it. Task list annotated at 10:11:47Z; original kept verbatim, no checkbox moved, no label changed.
The row reads "the sweep derived the transition at 11:37:30Z … so the live labels are now
enhancement+post-merge+scope:packaging". That was exact at 11:37:30Z — it is the derivation's own output. #54 now carries four labels:enhancement+post-merge+scope:ci+scope:packaging.scope:ciwas added at 2026-09-03T01:54:59Z by triage (claude-bot-andresmgsl), in the same pass that added it to #60.Two things make this worth recording rather than just fixing.
An enumeration of a live label set claims completeness, so an ADDITION falsifies it. Every state-rot shape recorded on this board so far fired on a removal or a replacement — a label dropped, a section rewritten, a file's lines pushed down. A set that gains a member leaves every named element still true; the sentence is falsified only by what it does not say. Checking each named label individually returns three passes and no signal.
A label mint is invisible to every wake condition this body carries. It merges nothing, moves no queue state, touches no file, and opens no PR. The re-arm rule for stale line citations is keyed on "a merge touches a file an open body cites"; the rot-sweep rules are keyed on merges and closes. None of them can see triage editing a label. Triage's own writes are a rot source that triage's own wake conditions do not cover — the mint at 01:54:59Z sat unswept through every tick since.
The row immediately above states the discipline this one missed. The #23 row ends "This row records the transition, not the live state." The #54 row records a transition and then asserts the live state in the same sentence, one line below. Same pattern as the citation drift found here earlier today, where the paragraph carrying stale line numbers was the paragraph that corrected another repo's stale line numbers: when a body demonstrates awareness of a failure mode, check the line beside it first.
Nothing else on the board carries this shape — all five open bodies were grepped for live label-set enumerations and for the adjective forms of queue state in the same pass. The other two hits found were on #36 (
## Dependenciesand## Labels, both asserting it is claimable when it has beenpost-mergesince 2026-08-31T19:49:06Z) and are annotated there. #27's own earlier hits — the2026-08-20record at the top of finding 1, "It wasreadyand unclaimed for three minutes", and the self-correctedpost-mergesentence in the #56 paragraph — are all explicitly dated or corrected in place and were left alone.Task list otherwise walked live and correct: #26/#24/#25/#1/#23 all
closedand ticked; #54openand unticked. No row moves.Triage, 2026-09-03 — the
#1task-list row's closing sentence had three claims and all three had inverted. Body annotated in place, original kept verbatim, nothing deleted. No label change, no row moved, no state change.The sentence
The
#1row ends:Measured live this tick:
v1.4.0at523a4558→ run 735,release.ymljobdeb,push,failure, started 08:13:15Zv*tag has ever been pushed here"v1.3.0f4b0bdb2026-07-26T21:37:32Z (predates the row);v1.4.0523a45582026-09-01T08:12:57Z;v1.5.0088e7e2d2026-09-02T22:29:15ZGET /issues/32→closed; its L245- [ ] v1.4.0 is tagged by @andres and release.yml runs on itRuns 735 and 990 are still the only two runs
release.ymlhas ever had, and both arefailure— 735 atscripts/publish-deb.shL33 witherror: no token(secrets.RELEASE_TOKENempty), 990 one step later on403 … required scope(s): [read:user]after the registry step returned201. So the criterion is measured false, not unfired, and has been for two days.Why no grep on this board could reach it
#32resolves. It is a real issue, it really does contain the two post-merge criteria this row names, and its own## Dependenciesstill reads "this issue's post-merge criteria 1 and 2 name the same wake, which is why nothing was stranded by closing it". A reader who follows the cross-reference to check it gets a confirmation — from a closed issue whose criterion 1 never got ticked. Same self-confirming failure mode as the last two ticks' findings, third instance running.And the wake: a close merges nothing, moves no queue, mints no label and touches no file. Every re-arm rule recorded for this board is keyed on merges (or, since yesterday, on label mints). #32 closed on 2026-09-01 and this row went unswept through every tick since. New standing rule: re-check every "carried on / tracked on / owned by " cross-reference in an open body whenever any issue on this board closes. Nine such cross-references exist across the five open bodies; the other eight were walked this tick and point at live issues or at closes they already state.
Checked and deliberately not changed
Board
Unchanged by this write: 5 open / 0 open PRs,
blocked0,claimed0, zero assignees, 28 labels all true,main2230ca25. Task list re-walked live — #26/#24/#25/#1/#23closedand ticked, #54openand unticked; no row moved.epic_referencesandunchecked_criteriareplayed at the0.6.3pin over the old and the new body: identical output (7 local refs, 6 rows, 1 unchecked), so the insertion is invisible to the sweep. Body re-fetched after the PATCH and byte-identical to what was sent.Triage, 2026-09-03 — correction to comment 34561, posted two minutes earlier. Its count was wrong. The corrected walk is below; nothing else in that comment changes, and the annotation in the body is unaffected.
34561 says "Nine such cross-references exist across the five open bodies; the other eight were walked this tick." There are sixteen, not nine. The number was written from a partial grep and not re-counted before posting — the exact failure this tick's finding is about, so it is corrected here rather than quietly. The full walk, re-run against the live bodies after both of this tick's writes, with each named item's state re-fetched:
openopen, and the sentence says soclosed2026-08-24T18:15:11Z, statedopenclosed2026-08-05, statedLine numbers are against the bodies as they stand after this tick's insertions.
Item 12, recorded rather than edited
L461 reads "#32's Spec now carries the matching record … so the fact sits both on the issue that publishes and on the epic that owns this file's open question, and neither can go stale alone." Both halves of the redundancy still physically exist — a closed body cannot drift, so in the literal sense "neither can go stale" is more true now, not less. What has changed is that the issue that publishes is no longer #32; it is #56, which carries its own version of that record (its post-merge checkpoint table reads the release page as "the notes are published", which is the stricter form). The sentence is therefore accurate about the two sites it names and misleading only about which of them is live. That is a weaker defect than items 2 and 13 and it is one this comment now covers, so the body is left alone. Do not re-correct L461 without a new reason.
The rule this produced
"There are N of them" is a tally, and a tally is the third rot shape recorded on this board. Writing one from a grep's first pass, in the same comment that argues enumerations must be checked for completeness, is the mistake it warns about. Count from the walk, not from the grep, and re-run the count against the post-write bodies before posting.
Last child closed — closing the epic
#54's AC6 was met by run 1106: the converted door published
v1.5.0end to end — release id 66,stoke_1.5.0_all.debattached, notes carrying the 713-byte1.5.0CHANGELOG.mdsection, and the log takes the create branch so nothing was hand-made. #54 is closed, its task-list row is ticked, and this epic closes with it under the contract in its own Notes: "The epic is #54's progress view, so it closes when #54 closes and not on its own criteria." All four acceptance criteria and all six task-list rows are ticked. #56 and #60 closed on the same run in the same tick.Nothing is stranded. Every child's residual criteria were discharged on their own bodies before the closes: #54's AC6 (the release page), #56's criteria 2 and 3 (the release page, and the clean-container
apt-get install→stoke --version1.5.0 that triage performed this tick), #60's post-merge criterion (the scope set proven necessary run-by-run and sufficient on 1106). #36 stays open atpost-mergeand is not a child of this epic.One correction carried onto this body, because this is the epic a future reader opens: the "re-run of run 990's job" wake condition triage wrote onto four issues named an act this Forgejo cannot perform —
runs/{id}/rerun,tasks/{id}/rerun,rerun-failed-jobsandjobs/0/rerunall 404. The work was recovered by the act those four bodies forbade, a tag delete-and-re-push, and it was harmless only because the tag came back byte-identical andpublish-deb.shtolerates the repeat409. A wake condition names an act — verify the act is performable before writing it onto four bodies.