epic: the five CLI gaps — priority order, and the two spec rescopes (#1, #23) that made them buildable #27

Closed
opened 2026-08-05 10:36:14 +00:00 by claude-bot-andresmgsl · 37 comments

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 value
today 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's
own 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
curl with the token pasted in, which is the exact credential handling the
CLI 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 stoke 1.2.0 installed
from the Forgejo Debian registry and main at 1.3.0.

Spec

Priority order

P issue why here size
P1 #26issue create cannot set labels Recurs on every issue any agent mints, and does active harm: the ceremony sweep stamps needs-triage in 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. small
P1 #24repo create cannot target an org — landed Every other repo-scoped command already takes -o, --owner; create was the odd one out. Shipped in !35, merged 2026-08-21T23:18:38Z (c09943e); #24 closed the same minute. small
P2 #25release create cannot upload assets — landed Blocked mirroring any release whose payload is an artifact — crew's 0.1.0/0.1.1 are 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's Closes #25. The id wrinkle is answered directly: release create now prints the numeric release id, and release upload resolves a tag to it. medium
P2 #1 — distribute as an apt package — landed Rescoped 2026-08-18 per finding 1 — buildable as specced. The infrastructure already existed; what was left was the [trusted=yes] verification gap. Shipped in !38, merged 2026-08-31T16:05:04Z by @andres (fb5cb474, head 2efc76f2, all three panelists APPROVED at that head); @andres closed #1 by hand one second later rather than letting it derive to post-merge, since !38 says Refs #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. small
P3 #23 — no repo sync Spec revised 2026-08-18 per finding 2 — buildable as specced. Narrowed to the undiverged mirror, with the diverged-tree and pull-mirror shapes ruled explicitly out of scope. Still the largest of the five. large

Finding 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 stokeii stoke 1.2.0 all, installed from the registry;
  • the Forgejo Debian registry serves stoke 1.3.0
    (/api/v1/packages/heavy-dutydebian stoke 1.3.0);
  • the signing key is published and returns a real PGP block
    (/api/packages/heavy-duty/debian/repository.key → HTTP 200, 1631 bytes);
  • that key is installed at /etc/apt/keyrings/forgejo-heavy-duty.asc;
  • release v1.3.0 exists 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:

/etc/apt/sources.list.d/…:
  deb [trusted=yes] https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian stable main

[trusted=yes] disables signature verification. The keyring is installed
and unused; the signed-by= form #1's own acceptance criteria specify is not
what is deployed. This is precisely the failure open PR #21 describes — the
install-apt.sh [trusted=yes] fallback being "too broad" and firing on
failures that are not signature failures, permanently disabling verification.

Correction, triage 2026-09-03: #21 has not been an open PR since 2026-08-30T08:59:29Z. It
closed 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 stoke lists only /var/lib/dpkg/status and
a 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 update or a repo the client is
failing 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, confirm
signed-by= verification end to end on a clean container, explain the version
skew, 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 sync as fast-forward only, "refusing a non-fast-forward
rather 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/ceremony on this forge has diverged on purpose: 30 commits of
Forgejo-compatibility work that upstream does not have and must never lose
(ceremony#188/#191/#194/#195). Upstream is 157 commits ahead. Their
merge-base is real — 84bb1a4 — so the sync is a genuine three-way merge
producing 18 conflict hunks, five of which need a human or agent decision. It
is tracked as ceremony#197/#198.

A fast-forward-only repo sync would refuse that repo forever, and a
--force would destroy the delta. Neither is the operation actually needed.

So #23 needs its spec revised before it is buildable. The honest split:

  • repo sync handles the undiverged mirror — fast-forward or refuse, as
    designed. Legitimate and useful; just say plainly that it does not serve a
    tree that carries local commits.
  • A diverged tree needs a documented merge procedure, not a CLI verb. That
    work is ceremony#200, and it belongs there, not here.
  • The pull-mirror alternative #23 already raises (mirror: true on 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 in
which every command group is chained (repo at L326, release at L985) — plus src/api.js
and a README.md section. #23 in particular adds a subcommand to the same repo group #24
modifies.

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):

issue key the machine reads
#23 repo sync
#24 repo create --owner
#25 release create --asset / release upload

Three 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:conflict is derived from
Forgejo's mergeable boolean, which is false for every draft PR whether or not it
conflicts, 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:conflict as evidence of same-file contention
until #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 two
minutes 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. So
the caveat is spent: at 0.6.3 blocker:conflict does 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 ready and 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). The
shared-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 d6a21c9d at 2026-09-02T11:22:58Z. Re-measured at main 2230ca25: .forgejo/workflows/release.yml is 43 lines and contains 0 occurrences of curl — it was 56 lines with 3 at 967efa02, 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 to scripts/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.yml L47-55 still does all of it by hand"; and (4) "So the criterion stays unticked". That criterion is ticked## Acceptance criteria bullet 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 three curls 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 main at 033a40c, in the tree rather than
from the PR descriptions:

gap surface on main shipped by
create an org repo src/cli.js L458 -o, --owner on repo create #24 / !35
label an issue src/cli.js L721 --label <name...> on issue create #26 / !29
attach a release asset src/cli.js L1104-1105 --asset / --asset-name, L1147 release upload, src/api.js L277 uploadReleaseAsset #25 / !37

(Every src/cli.js number in this table is a 033a40c number and all four are spent at main;
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 as c34a8b04) inserted a new repo sync command block at
src/cli.js L455-498, above everything this table cites, so cli.js went 1742 → 1786
lines and every row below shifted by exactly +44: --label <name...> L721 → L765,
--asset L1104 → L1148, --asset-name L1105 → L1149, .command('upload') L1147 →
L1191. src/api.js L277 uploadReleaseAsset is untouched (393 lines at both refs) and is
still 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.js L458 is repo create's
.option('-o, --owner <owner>', 'repository owner (authenticated user or organization)'); at
2230ca25 that same line number is .requiredOption('-o, --owner <owner>', 'repository owner')
— which belongs to repo sync. The stale citation still lands on an -o, --owner line, so it
confirms itself to a reader who checks the flag and not the enclosing command. repo create's
own -o, --owner is at L502 at main. Nothing in the table's claim changed — all three
capabilities still ship and the epic's conclusion is unaffected; only the numbers moved.

The fact on the ground is not. .forgejo/workflows/release.yml L47-55 still does all
of it by hand — curl to fetch or create the release, then
curl -X POST -F "attachment=@$DEB", each with secrets.RELEASE_TOKEN in an
Authorization: token header. That is the exact shape this epic's context paragraph names
as 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.yml would
have 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 stoke reads its own config. The
checkout makes node src/cli.js available 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. The
authentication question is answerable from the repo, and the answer needs no source change:

  • auth login takes -t, --token <token> and --token-file <path>, and either one
    suppresses both interactive prompts — src/cli.js guards each on !token
    (if (!username && !token), if (!password && !token)), so neither is read.
  • The global --config <path> flag (src/cli.js L24-26, a preAction hook that sets
    STOKE_CONFIG_FILE) puts that config anywhere, so CI need not write into $HOME.
  • Run end to end with stdin closed, at 9586d2c:
    node src/cli.js --config <tmp> auth login --url … --token …
    Authenticated as claude-bot-andresmgsl using provided token, exit 0; then
    node src/cli.js --config <tmp> release list -o heavy-duty -r stokev1.3.0, exit 0,
    no prompt. The stored config is {url, login, username, email, token, tokenId: null}.
  • Prefer --token-file over --token: the latter puts the secret in argv.

The CLI covers the whole block, and the RELEASE_ID plumbing disappears rather than being
ported.
release view --tag --json replaces the L47 lookup; release create
(--tag/--title/--draft/--prerelease) replaces L49-51 and prints the id; release upload --tag --asset replaces L53-55 and resolves the tag to the id itself (getReleaseByTag,
src/cli.js L1203), so no step has to carry RELEASE_ID to the next. release create also
takes --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.js available with no install. src/cli.js L3 requires
commander, the package's sole runtime dependency; on a bare checkout it exits 1 with
MODULE_NOT_FOUND (measured). It is harmless in this workflow only, because L28 already
runs npm ci && npm test before the release steps — but the clause is false in general and
was load-bearing for the conclusion, so it is corrected here rather than quietly dropped.

The trap this leaves for whoever converts it: release.yml L35 already sets
STOKE_TOKEN, in the step immediately above the curls, which reads as though the CLI is
already authenticated in this file. It is not. STOKE_TOKEN is honoured by exactly one line
in the tree — scripts/publish-deb.sh:30 — and nothing under src/ ever reads it:
src/config.js resolves the token only from a config file, and every loadConfig() call
site in cli.js goes through it. Exporting STOKE_TOKEN and calling the CLI yields an
unauthenticated 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 a
bespoke release workflow at all. heavy-duty/crew and heavy-duty/provider-seeker — the
org's only other release-shipping repositories — hand-roll nothing, because their
.github/workflows/release.yml is a four-line
uses: heavy-duty/ceremony/.github/workflows/release.yml@<tag> (crew at 0.6.2,
provider-seeker at 0.6.3). That reusable workflow contains zero curl calls: it goes
through lib/forge.shforge_select, which detects this forge from GITHUB_API_URL's
/api/v1 shape and dispatches to the Forgejo backend, and it authenticates with the ambient
GH_TOKEN: ${{ github.token }} rather than a pasted secrets.RELEASE_TOKEN. This
repository's own tree already records the route, and a cost of not taking it
:
.ceremony/README.md L9-14 says stoke "keeps its own tag-driven
.forgejo/workflows/release.yml"
and that "docs-sync --fix cannot run until or unless a
future 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.yml after the tag exists and before the publish,
with the hook contract drop finished files into $RELEASE_ASSETS_DIR. crew and
provider-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 retire secrets.RELEASE_TOKEN from this file:
scripts/publish-deb.sh PUTs the .deb to /api/packages/heavy-duty/debian/pool/…, a
package-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.yml at tag 0.6.3 reaches the tag door's publish step with no
existence check — L385 is forge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}"
straight after the artifact hook — and forge_release_create at 0.6.3
(lib/forge-forgejo.sh) POSTs the release with draft:false first, then uploads each
asset; 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.yml
L47-52 and "this repository's own three curls", both of which !55 deleted
— at 088e7e2d
git grep -c curl .forgejo/workflows/release.yml returns 0. The property survived the
conversion and only moved: scripts/publish-release.sh L44-49
is get-or-create (release viewrelease upload, else release create), so a re-run finds
the existing release and re-attaches, and scripts/publish-deb.sh L42-51 treats 409 as
Already published without exiting 1, so the registry step is idempotent under the same re-run.
(scripts/publish-deb.sh L42-51 is a d6a21c9d/088e7e2d citation and is spent at main — kept
verbatim, because the ref it is true at is the one this argument runs on. Triage, 2026-09-03.)

!59 merged as
bbde478
at 2026-09-03T07:30:12Z and inserted a six-line heredoc above this block, so at main
(2230ca25) the file is 60 lines, the case runs L50-59 and the 409 arm is L52. At
088e7e2d
— 52 lines, the commit v1.5.0 points at and therefore the tree a re-run of run 990 replays
it is L42-51 with the 409 arm at L44, exactly as written. The claim is unchanged at both
refs (409) echo "Already published (409): …", no exit 1), and this citation is load-bearing for
the re-run recovery, so read it at 088e7e2d, not at main.
This is no longer a paper argument. Run
990 on v1.5.0 published
stoke 1.5.0 to the registry and then failed on the next step, which is precisely the
strand-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 the
release 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 a
draft, 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.3
is false, so the fix lives only on ceremony main (VERSION = 0.6.4-dev, head
91aee7f8), and the tag list is still x-total-count: 8 with 0.6.3 on top. Measured
against forgejo.heavyduty.builders, not GitHub's same-named repository. The cost is
therefore dated rather than structural — a 0.6.4 carrying #271 erases it — and it does not
reverse 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 stoke
carries 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.sh
against forgejo.heavyduty.builders: c4315c2 "publish Forgejo releases
atomically"
(2026-08-30T09:25:43Z, the ceremony#271 fix named above),
734676a "guard tag release publication" (2026-08-30T09:27:08Z), which adds
a 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: 8 with 0.6.3 (8f0ef796, 2026-08-26T20:18:16Z) on
top, and main is 91aee7f8 at VERSION 0.6.4-dev.

And there is a sixth cost, which is structural rather than dated — a 0.6.4
does not erase it.
Ceremony's tag door asserts that the pushed tag equals the
tree's version verbatim
:
.github/workflows/release.yml@0.6.3 L347
is if [ "$GITHUB_REF_NAME" != "$ver" ]"does not match the tree's version —
creating nothing"
, exit 1, where ver comes from version_read
(lib/version.sh L16). Every release tag stoke has ever pushed is
v-prefixed
v1.3.0, v1.4.0 — and .forgejo/workflows/release.yml L16-17
filters on tags: ['v*'], while package.json holds 1.5.0 (it read 1.4.0
when this paragraph was written; #56 bumped it at 088e7e2d, measured
2026-09-02T21:4xZ) and there is no VERSION file (GET /contents/VERSION?ref=088e7e2d
404, against a 200 control on package.json at the same ref). Under B, v1.5.0 would 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.0 was pushed at 088e7e2d on 2026-09-02T22:29:15Z by @claude-lead-andresmgsl — an annotated tag, 6ba87b48, message stoke 1.5.0 — so this repository has three v-prefixed release tags, not the two listed: v1.3.0, v1.4.0, v1.5.0 (GET /tagsx-total-count: 3, re-measured this tick). v1.5.0 is therefore no longer the hypothetical this clause treats it as: it exists, run 990 fired on it, and its .deb is live in the registry while its release page is not (GET /releases/tags/v1.5.0404; releases still x-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 still v-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 ## Notes gate below, and #54's acceptance criterion 6 which states "This repository now has three tags") — as with leaving two in ## Context, the stale copy was the one nobody re-read. Adopting B
therefore 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.Z from their
first release. Measured, not inferred: both siblings' callers were re-fetched
this tick (crew pins 0.6.2, provider-seeker 0.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.0 was tagged
2026-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.0 ships" — has therefore fired, and by this paragraph's own terms the
conversion becomes mintable as a fresh ready issue the moment the route is picked. It is
minted: #54, created
2026-09-02T09:14:16Z, carrying route A as a decided spec.
It was ready and unclaimed
for three minutes: @codex-bot-andresmgsl claimed it at 2026-09-02T09:17:59Z — read off
the label events and not the thread, ready removed 09:17:58Z, claimed added and
self-assigned 09:17:59Z — and opened
!55 at 09:19:44Z. The route
question this epic held is answered; the needs-ruling it carried is cleared in the same
comment 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 labeled event is
2026-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_ruling anchor to.

And the tagged run gave the route call evidence it did not have when it was written.
Run 735 — the first
time release.yml has ever fired — went red: 117/117 tests passed and the .deb built,
then the Publish to Debian registry step exited 1 at
scripts/publish-deb.sh:33
with error: no token, because secrets.RELEASE_TOKEN expanded to empty. The Create
release and attach .deb
step — the three hand-rolled curls this finding is about — never
executed, so v1.4.0 was published by hand. This does not decide A vs B, and it is
important that it is not read as doing so: the paragraph above already measured that
scripts/publish-deb.sh PUTs to a package-registry surface neither route touches, so
the 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 curl calls, recorded
2026-08-30T15:47Z.
The create at
release.yml L49-51
sends {"tag_name":"$TAG","name":"$TAG","draft":false,"prerelease":false} and
no body — and the step reads no file for one — so every release this
workflow publishes has an empty notes pane. That closes a loop:
scripts/build-deb.sh L60-66
writes the .deb's own changelog.gz as the single bullet "See the repository
release 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 with lib/changelog.sh, this repo has no
equivalent, and until #32 gained the task on 2026-08-30 it had no CHANGELOG.md
at all. The half of this that pointed at the curl calls is spent: release create
takes --body-file <path> (src/cli.js L1147-1148, read by readBodyOption L62-71), so
sending a body is a shipped capability and no longer waits on the conversion question
above. What remains is only the extraction--body-file reads a whole file verbatim, it
does not select a section — so the missing piece is a CHANGELOG.md section-extractor, and
it 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 sources lib/changelog.sh; its matcher is
awk '/^## / && $2 == ver'. Both files were fetched at tag 0.6.3 from
forgejo.heavyduty.builders and run against this repository's CHANGELOG.md at
9586d2c, unmodified
: changelog-section 1.4.0exit 0, 21 lines, the complete
### Added / ### Changed / ### Fixed body on stdout, nothing on stderr. The heading
## 1.4.0 — 2026-08-31 matches because the date lands in $3, not $2. So the extraction
is 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 at
all
, because ceremony's release workflow already publishes the version's own CHANGELOG.md
section 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.

  • #26issue create --label (P1)

  • #24repo create --owner (P1) — merged 2026-08-21 as !35; #24 closed

  • #25release 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 the Closes #25 was correct and nothing was owed to post-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 head 2efc76f2 (@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 derived post-merge move!38 says Refs #1 and 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 no v* 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 is
    carried 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.0 was pushed at 523a4558 and release.yml
    ran on it as run 735, the first time that workflow had ever fired. It went red 23 seconds in
    at scripts/publish-deb.sh L33 — error: no token, because secrets.RELEASE_TOKEN expanded
    to empty — so Create release and attach .deb never ran, and stoke 1.4.0 reached the
    registry and its release page by hand. A second v* tag has fired since: v1.5.0 at
    088e7e2d, 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 runs release.yml has
    ever 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.0 was pushed 2026-07-26T21:37:32Z, long before this row existed. The source claim on
    the issue this row is about states the precise thing instead — that v1.3.0 predates the
    workflow, 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.0 is tagged by @andres and release.yml runs on it" — still unchecked. The
    structure 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.

  • #23repo sync, spec narrowed per finding 2 (P3) — merged 2026-08-31 as !41 (c34a8b04); #23 closed the same second by its Closes #23. Built by @codex-bot-andresmgsl, claimed 2026-08-31T16:41Z. The panel approved at the merged head 316bec58 (@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 the Closes was correct and nothing was owed to post-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-rolled curls to stoke's own CLI and publish the version's CHANGELOG.md section 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 opens Refs #54 — so the close is triage's and not the merge's. Merged into main 2026-09-02T11:22:58Z as d6a21c9d, by @andres, at head ee88d7d3. The sweep derived the transition at 11:37:30Z — claimed removed, the assignee released, post-merge added — so the live labels are now enhancement+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 one
    surviving criterion (AC6) was discharged by run 1106,
    2026-09-03T20:52:30Z: the converted door published v1.5.0 end to end — release id 66,
    stoke_1.5.0_all.deb attached at 20:52:44Z, and the notes pane carrying the 713-byte 1.5.0
    CHANGELOG.md section produced by changelog-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 is
    enhancement+post-merge+scope:ci+scope:packaging. Nothing in the transition record
    above 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_references hands
each whole row to the state check, so any #N in a row counts as a child). The sweep
nudged 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 one
operator-gated criterion. Half of that gate is now spent, 2026-09-02T19:33:47Z: @andres set
RELEASE_TOKEN at the heavy-duty org level, so the criterion waits only on the next v* tag.
That half is spent too, and it did not discharge the gate (triage, 2026-09-02): v1.5.0 was pushed at 088e7e2d on 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 carries read: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-jobs and …/actions/runs/990/jobs/0/rerun all 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, missing write:repository) and run
1106 (success), both push events on v1.5.0 at 088e7e2d. It was
harmless: refs/tags/v1.5.0 still resolves to the same annotated tag object
6ba87b48acff896d47065a24b0cde7fe3c0d3373 at the same commit with the same tagger and date, and
publish-deb.sh's 409 arm 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 main as
088e7e2d. Its five pre-merge criteria are verified at that commit, the sweep derived its
transition at 20:47:58Z, and it now reads post-merge and unassigned with only its three
tag-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 metv1.5.0 resolves to 088e7e2d and
release.yml ran — and the third criterion's registry half is met as well: GET /api/v1/packages/heavy-duty?type=debian returns debian stoke 1.5.0, created 22:29:42Z. What is
left is the release page carrying the CHANGELOG.md section as its notes with stoke_1.5.0_all.deb
attached (GET /releases/tags/v1.5.0404, re-measured this tick), and a clean-container
install. Both wait on the re-run: run 990 is failure, and it died at Create release and attach
.deb
on 403 … required scope(s): [read:user] after the registry step had already returned
201. #54's AC6 shares that identical wake, so the two still move together — which is the only
part of this sentence that survived the tag. (This paragraph said ready and
unassigned 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 labeled
event'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; the
sixth row and this paragraph revised 2026-09-02.

Acceptance criteria

  • Every issue above is either closed or carries a body that reflects the
    findings here.
  • No workflow in this org needs a hand-rolled curl with a pasted token
    to create an org repo, label an issue, or attach a release asset.
    Ticked 2026-09-02 on !55's merge (d6a21c9d), which deleted the last
    one.
    The CLI could do all three as of 033a40c (2026-08-30), but
    release.yml L47-55 went on hand-rolling the asset attach until that
    merge; 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/lib
    file in all 9 repositories of heavy-duty on
    forgejo.heavyduty.builders was scanned — 91 files, matching curl
    with 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.yml L47/49/53 — in
    scope, 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/stoke scripts/publish-deb.sh L38, a PUT to
    /api/packages/heavy-duty/debian/pool/… — a package-registry surface
    that is none of the three and for which src/cli.js has no command
    group at all
    (no package command; the file's only registry token is
    the read:package/write:package scope string at L154);
    heavy-duty/ceremony .github/workflows/labels.yml L175/L190, a
    default-branch read and the sweep dispatch, on the ambient
    GITHUB_TOKEN and not a pasted secret; and heavy-duty/ceremony
    lib/forge-forgejo.sh L143/160/532/664/851, the engine's own forge
    transport — 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-equipment and stoke have default-branch
    commits 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 one
    token-bearing curl, scripts/publish-deb.sh L38 — the out-of-scope
    registry PUT — and .forgejo/workflows/release.yml no longer appears
    at all; office-equipment (82e32a9c) carries none; ceremony
    (91aee7f8) carries the same two out-of-scope files as before, though
    their line numbers have drifted with the tree
    (lib/forge-forgejo.sh now L143/160/532/536/664/855, recorded here on
    2026-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 L38 citations in this criterion — the out-of-scope
    list above and the re-measurement here — are spent at main. Triage, 2026-09-03; kept
    verbatim, 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
    bbde478
    at 2026-09-03T07:30:12Z, replacing publish-deb.sh's one-line empty-token guard with a
    six-line heredoc and pushing everything below it down by 8 lines. The token-bearing
    PUT is L38-40 at d6a21c9d and at 088e7e2d (52 lines) and L46-48 at
    2230ca25 (60 lines), with the Authorization: token header on L47. The
    measurement is untouched — only the two L38s moved:
    at main,
    grep -c 'Authorization: token' scripts/publish-deb.sh is still 1, it is still the
    package-registry PUT, and it is still out of scope. Re-resolved in the same pass and
    unchanged: src/cli.js L154 is still the read:package/write:package scope string,
    and heavy-duty/ceremony is still at 91aee7f8 with lib/forge-forgejo.sh curls at
    L143/160/532/536/664/855 exactly as corrected above.
    Host note, same trap as heavy-duty/ceremony on #36:
    heavy-duty is itself two organizations — 9 repositories on
    forgejo.heavyduty.builders against 28 on github.com, sharing
    five names (box, rig, cast, ceremony, crew) — and stoke
    exists 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 stoke on a clean container verifies signatures via
    signed-by=, and no [trusted=yes] is written by any path the user did
    not 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 InRelease is authentically signed but unparseable — the
    documented 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 — Ubuntu
    24.04 installs with the signed-by= source retained, Debian 13 refuses and
    leaves no source behind, and the opt-in is the only route to
    [trusted=yes]. Two honest limits on this tick, neither of which
    contradicts it: those transcripts were taken at 9cc9576, one commit
    before the merged head, and the delta at 2efc76f only narrows the
    opt-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 25519 is empty and the single
    [trusted=yes] write at scripts/install-apt.sh:164 sits behind both the
    exact /usr/bin/sqv + Malformed MPI condition and
    STOKE_ALLOW_UNVERIFIED_APT=1.
  • #23 states in its own body which of the three shapes (fast-forward
    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 an
asset 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 main rather than the
2026-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.

## 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/1) closed **2026-08-31T16:05:05Z** (merged as [!38](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/38), `fb5cb474`, 16:05:04Z) and [#23](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/23) closed **2026-08-31T18:42:40Z** (merged as [!41](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/41), `c34a8b04`, same second). All five are closed; **zero** of them remain open. Read at face value today 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's own summary line says *"Every issue this list references except #54 is closed"*. The epic's only open child is [#54](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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 `curl` with the token pasted in, which is the exact credential handling the CLI 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 `stoke` 1.2.0 installed from the Forgejo Debian registry and `main` at 1.3.0. ## Spec ### Priority order | P | issue | why here | size | |---|---|---|---| | **P1** | #26 — `issue create` cannot set labels | Recurs on **every** issue any agent mints, and does active harm: the ceremony sweep stamps `needs-triage` in 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. | small | | **P1** | #24 — `repo create` cannot target an org — **landed** | Every other repo-scoped command already takes `-o, --owner`; `create` was the odd one out. Shipped in !35, merged 2026-08-21T23:18:38Z (`c09943e`); #24 closed the same minute. | small | | **P2** | #25 — `release create` cannot upload assets — **landed** | Blocked mirroring any release whose payload is an artifact — crew's `0.1.0`/`0.1.1` are 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's `Closes #25`. The id wrinkle is answered directly: `release create` now prints the numeric release id, and `release upload` resolves a tag to it. | medium | | **P2** | #1 — distribute as an apt package — **landed** | **Rescoped 2026-08-18 per finding 1 — buildable as specced.** The infrastructure already existed; what was left was the `[trusted=yes]` verification gap. Shipped in !38, merged 2026-08-31T16:05:04Z by @andres (`fb5cb474`, head `2efc76f2`, all three panelists APPROVED at that head); @andres closed #1 by hand one second later rather than letting it derive to `post-merge`, since !38 says `Refs #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. | small | | **P3** | #23 — no `repo sync` | **Spec revised 2026-08-18 per finding 2 — buildable as specced.** Narrowed to the undiverged mirror, with the diverged-tree and pull-mirror shapes ruled explicitly out of scope. Still the largest of the five. | large | ### Finding 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; - the Forgejo Debian registry serves stoke **1.3.0** (`/api/v1/packages/heavy-duty` → `debian stoke 1.3.0`); - the signing key is published and returns a real PGP block (`/api/packages/heavy-duty/debian/repository.key` → HTTP 200, 1631 bytes); - that key is installed at `/etc/apt/keyrings/forgejo-heavy-duty.asc`; - release `v1.3.0` exists 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: ``` /etc/apt/sources.list.d/…: deb [trusted=yes] https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian stable main ``` `[trusted=yes]` **disables signature verification**. The keyring is installed and unused; the `signed-by=` form #1's own acceptance criteria specify is not what is deployed. This is precisely the failure open PR #21 describes — the `install-apt.sh` `[trusted=yes]` fallback being "too broad" and firing on failures that are not signature failures, permanently disabling verification. **Correction, triage 2026-09-03: `#21` has not been an *open* PR since 2026-08-30T08:59:29Z.** It closed **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 stoke` lists only `/var/lib/dpkg/status` and a 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 update` or a repo the client is failing 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, confirm `signed-by=` verification end to end on a clean container, explain the version skew, 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 sync` as **fast-forward only**, "refusing a non-fast-forward rather 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/ceremony` on this forge has **diverged on purpose**: 30 commits of Forgejo-compatibility work that upstream does not have and must never lose (ceremony#188/#191/#194/#195). Upstream is 157 commits ahead. Their `merge-base` is real — `84bb1a4` — so the sync is a genuine three-way **merge** producing 18 conflict hunks, five of which need a human or agent decision. It is tracked as ceremony#197/#198. A fast-forward-only `repo sync` would refuse that repo forever, and a `--force` would destroy the delta. Neither is the operation actually needed. **So #23 needs its spec revised before it is buildable.** The honest split: - `repo sync` handles the **undiverged mirror** — fast-forward or refuse, as designed. Legitimate and useful; just say plainly that it does not serve a tree that carries local commits. - A diverged tree needs a documented **merge** procedure, not a CLI verb. That work is ceremony#200, and it belongs there, not here. - The pull-mirror alternative #23 already raises (`mirror: true` on 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 in which every command group is chained (`repo` at L326, `release` at L985) — plus `src/api.js` and a `README.md` section. #23 in particular adds a subcommand to the same `repo` group #24 modifies. **It does not owe collision edges.** [TRIAGE.md's collision rule](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/branch/main/TRIAGE.md#the-issue-contract) (#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): | issue | key the machine reads | |---|---| | #23 | `repo sync` | | #24 | `repo create --owner` | | #25 | `release create --asset / release upload` | Three 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:conflict` is derived from Forgejo's `mergeable` boolean, which is `false` for every **draft** PR whether or not it conflicts, so the label fired on all of them and could not be told apart from a real rebase — [#36 defect 6](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/36), 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:conflict` as evidence of same-file contention until #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 two minutes 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. So the caveat is spent: at `0.6.3` `blocker:conflict` does 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 `ready` and 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). The shared-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](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/55), merged as [`d6a21c9d`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/d6a21c9d9e1a4699d4b07cc76aecaac5ab31e847) at 2026-09-02T11:22:58Z. Re-measured at `main` [`2230ca25`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/2230ca25): `.forgejo/workflows/release.yml` is **43 lines** and contains **0** occurrences of `curl` — it was **56 lines with 3** at [`967efa02`](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/967efa027cb22d429b9350b71091c4caa5bf45ee/.forgejo/workflows/release.yml), 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 to `scripts/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.yml` L47-55 **still does** all of it by hand"*; and (4) *"**So the criterion stays unticked**"*. **That criterion is ticked** — `## Acceptance criteria` bullet 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/54) exists; only its tense is spent. **Nothing below this block is edited**: the later paragraphs go on describing the three `curl`s 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 `main` at `033a40c`, in the tree rather than from the PR descriptions: | gap | surface on `main` | shipped by | |---|---|---| | create an org repo | `src/cli.js` L458 `-o, --owner` on `repo create` | #24 / !35 | | label an issue | `src/cli.js` L721 `--label <name...>` on `issue create` | #26 / !29 | | attach a release asset | `src/cli.js` L1104-1105 `--asset` / `--asset-name`, L1147 `release upload`, `src/api.js` L277 `uploadReleaseAsset` | #25 / !37 | **(Every `src/cli.js` number in this table is a `033a40c` number and all four are spent at `main`; 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/41) (#23, `repo sync`, merged 2026-08-31 as `c34a8b04`) inserted a new `repo sync` command block at `src/cli.js` **L455-498**, above everything this table cites, so `cli.js` went 1742 → **1786** lines and every row below shifted by exactly **+44**: `--label <name...>` L721 → **L765**, `--asset` L1104 → **L1148**, `--asset-name` L1105 → **L1149**, `.command('upload')` L1147 → **L1191**. `src/api.js` L277 `uploadReleaseAsset` is untouched (393 lines at both refs) and is still 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.js` L458 is `repo create`'s `.option('-o, --owner <owner>', 'repository owner (authenticated user or organization)')`; at `2230ca25` that same line number is `.requiredOption('-o, --owner <owner>', 'repository owner')` — which belongs to **`repo sync`**. The stale citation still lands on an `-o, --owner` line, so it *confirms itself* to a reader who checks the flag and not the enclosing command. `repo create`'s own `-o, --owner` is at **L502** at `main`. **Nothing in the table's claim changed** — all three capabilities still ship and the epic's conclusion is unaffected; only the numbers moved. **The fact on the ground is not.** `.forgejo/workflows/release.yml` L47-55 still does all of it by hand — `curl` to fetch or create the release, then `curl -X POST -F "attachment=@$DEB"`, each with `secrets.RELEASE_TOKEN` in an `Authorization: token` header. That is the exact shape this epic's context paragraph names as 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.yml` would have 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 `stoke` reads its own config. The checkout makes `node src/cli.js` available 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.** The authentication question *is* answerable from the repo, and the answer needs no source change: - `auth login` takes `-t, --token <token>` and `--token-file <path>`, and either one suppresses both interactive prompts — `src/cli.js` guards each on `!token` (`if (!username && !token)`, `if (!password && !token)`), so neither is read. - The global `--config <path>` flag (`src/cli.js` L24-26, a `preAction` hook that sets `STOKE_CONFIG_FILE`) puts that config anywhere, so CI need not write into `$HOME`. - Run end to end with stdin closed, at `9586d2c`: `node src/cli.js --config <tmp> auth login --url … --token …` → `Authenticated as claude-bot-andresmgsl using provided token`, exit 0; then `node 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}`. - Prefer `--token-file` over `--token`: the latter puts the secret in `argv`. **The CLI covers the whole block, and the `RELEASE_ID` plumbing disappears rather than being ported.** `release view --tag --json` replaces the L47 lookup; `release create` (`--tag/--title/--draft/--prerelease`) replaces L49-51 and prints the id; `release upload --tag --asset` replaces L53-55 and resolves the tag to the id itself (`getReleaseByTag`, `src/cli.js` L1203), so no step has to carry `RELEASE_ID` to the next. `release create` also takes `--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.js` available with no install. `src/cli.js` L3 requires `commander`, the package's sole runtime dependency; on a bare checkout it exits 1 with `MODULE_NOT_FOUND` (measured). It is harmless *in this workflow only*, because L28 already runs `npm ci && npm test` before the release steps — but the clause is false in general and was load-bearing for the conclusion, so it is corrected here rather than quietly dropped. **The trap this leaves for whoever converts it:** `release.yml` L35 already sets `STOKE_TOKEN`, in the step immediately above the `curl`s, which reads as though the CLI is already authenticated in this file. It is not. `STOKE_TOKEN` is honoured by exactly one line in the tree — `scripts/publish-deb.sh:30` — and **nothing under `src/` ever reads it**: `src/config.js` resolves the token only from a config *file*, and every `loadConfig()` call site in `cli.js` goes through it. Exporting `STOKE_TOKEN` and calling the CLI yields an unauthenticated client, not a working one. **A second route exists, and this finding had never named it — measured 2026-09-01.** Porting the three `curl`s to stoke's own CLI is one way; the other is to stop carrying a bespoke release workflow at all. `heavy-duty/crew` and `heavy-duty/provider-seeker` — the org's only other release-shipping repositories — hand-roll nothing, because their `.github/workflows/release.yml` is a four-line `uses: heavy-duty/ceremony/.github/workflows/release.yml@<tag>` (crew at `0.6.2`, provider-seeker at `0.6.3`). That reusable workflow contains **zero** `curl` calls: it goes through `lib/forge.sh` → `forge_select`, which detects this forge from `GITHUB_API_URL`'s `/api/v1` shape and dispatches to the Forgejo backend, and it authenticates with the ambient `GH_TOKEN: ${{ github.token }}` rather than a pasted `secrets.RELEASE_TOKEN`. **This repository's own tree already records the route, and a cost of not taking it**: `.ceremony/README.md` L9-14 says stoke *"keeps its own tag-driven `.forgejo/workflows/release.yml`"* and that *"`docs-sync --fix` cannot run until or unless a future 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.yml` after the tag exists and before the publish, with the hook contract *drop finished files into `$RELEASE_ASSETS_DIR`*. crew and provider-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 retire `secrets.RELEASE_TOKEN` from this file: `scripts/publish-deb.sh` PUTs the `.deb` to `/api/packages/heavy-duty/debian/pool/…`, a package-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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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.yml` at tag `0.6.3` reaches the tag door's publish step with no existence check — L385 is `forge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}"` straight after the artifact hook — and `forge_release_create` at `0.6.3` (`lib/forge-forgejo.sh`) POSTs the release with `draft:false` **first**, then uploads each asset; 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.yml` L47-52 and "this repository's own three `curl`s", both of which !55 deleted** — at `088e7e2d` `git grep -c curl .forgejo/workflows/release.yml` returns **0**. The property survived the conversion and only moved: [`scripts/publish-release.sh` L44-49](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/088e7e2d66089007a93a00213f2173d0d9142580/scripts/publish-release.sh#L44) is get-or-create (`release view` → `release upload`, else `release create`), so a re-run finds the existing release and re-attaches, and `scripts/publish-deb.sh` L42-51 treats **409** as `Already published` without exiting 1, so the registry step is idempotent under the same re-run. **(`scripts/publish-deb.sh L42-51` is a `d6a21c9d`/`088e7e2d` citation and is spent at `main` — kept verbatim, because the ref it is true at is the one this argument runs on. Triage, 2026-09-03.)** [!59](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/59) merged as [`bbde478`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/bbde478157b839eb421577dc19f51c9aabe31791) at 2026-09-03T07:30:12Z and inserted a six-line heredoc above this block, so at `main` (`2230ca25`) the file is **60 lines**, the `case` runs **L50-59** and the `409` arm is **L52**. At [`088e7e2d`](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/088e7e2d66089007a93a00213f2173d0d9142580/scripts/publish-deb.sh#L42) — 52 lines, the commit `v1.5.0` points at and therefore the tree a **re-run of run 990 replays** — it is **L42-51** with the `409` arm at **L44**, exactly as written. The claim is unchanged at both refs (`409) echo "Already published (409): …"`, no `exit 1`), and this citation is load-bearing for the re-run recovery, so read it at `088e7e2d`, not at `main`. **This is no longer a paper argument.** Run [990](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/990) on `v1.5.0` published `stoke 1.5.0` to the registry and *then* failed on the next step, which is precisely the strand-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 the release 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 a **draft**, 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.3` is **false**, so the fix lives only on ceremony `main` (`VERSION` = `0.6.4-dev`, head `91aee7f8`), and the tag list is still `x-total-count: 8` with `0.6.3` on top. Measured against `forgejo.heavyduty.builders`, not GitHub's same-named repository. **The cost is therefore dated rather than structural — a `0.6.4` carrying #271 erases it — and it does not reverse 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 stoke carries 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.sh` against `forgejo.heavyduty.builders`: **`c4315c2`** *"publish Forgejo releases atomically"* (2026-08-30T09:25:43Z, the ceremony#271 fix named above), **`734676a`** *"guard tag release publication"* (2026-08-30T09:27:08Z), which adds a *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: 8` with **`0.6.3`** (`8f0ef796`, 2026-08-26T20:18:16Z) on top, and `main` is `91aee7f8` at `VERSION` `0.6.4-dev`. *And there is a sixth cost, which is structural rather than dated — a `0.6.4` does not erase it.* Ceremony's tag door asserts that the pushed tag **equals the tree's version verbatim**: [`.github/workflows/release.yml@0.6.3` L347](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/tag/0.6.3/.github/workflows/release.yml#L347) is `if [ "$GITHUB_REF_NAME" != "$ver" ]` → *"does not match the tree's version — creating nothing"*, exit 1, where `ver` comes from `version_read` (`lib/version.sh` L16). **Every release tag stoke has ever pushed is `v`-prefixed** — `v1.3.0`, `v1.4.0` — and `.forgejo/workflows/release.yml` L16-17 filters on `tags: ['v*']`, while `package.json` holds `1.5.0` (it read `1.4.0` when this paragraph was written; #56 bumped it at `088e7e2d`, measured 2026-09-02T21:4xZ) and there is no `VERSION` file (`GET /contents/VERSION?ref=088e7e2d` **404**, against a **200** control on `package.json` at the same ref). Under B, `v1.5.0` would 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.0` was pushed at `088e7e2d` on **2026-09-02T22:29:15Z** by @claude-lead-andresmgsl — an annotated tag, `6ba87b48`, message `stoke 1.5.0` — so this repository has **three** `v`-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.0` is therefore no longer the hypothetical this clause treats it as: it exists, [run 990](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/990) fired on it, and its `.deb` is live in the registry while its release page is not (`GET /releases/tags/v1.5.0` → **404**; releases still `x-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 still `v`-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 `## Notes` gate below, and #54's acceptance criterion 6 which states *"This repository now has **three** tags"*) — as with `leaving two` in `## Context`, the stale copy was the one nobody re-read. Adopting B therefore 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.Z` from their first release. Measured, not inferred: both siblings' callers were re-fetched this tick (crew pins `0.6.2`, provider-seeker `0.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.0` was tagged 2026-09-01T08:12:57Z at [`523a4558`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/523a45587f12298ded00a988318a2c0ce25d2f85), the registry serves `stoke 1.4.0`, and #32 is closed.** The default this epic recorded — *"after `v1.4.0` ships"* — has therefore fired, and by this paragraph's own terms the conversion becomes mintable as a fresh `ready` issue the moment the route is picked. **It is minted: [#54](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/54), created 2026-09-02T09:14:16Z, carrying route A as a decided spec.** It was `ready` and unclaimed for three minutes: @codex-bot-andresmgsl claimed it at **2026-09-02T09:17:59Z** — read off the label events and not the thread, `ready` removed 09:17:58Z, `claimed` added and self-assigned 09:17:59Z — and opened [!55](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/55) at 09:19:44Z. The route question this epic held is answered; the `needs-ruling` it carried is cleared in the same comment 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 `labeled` event is **2026-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_ruling` anchor to. **And the tagged run gave the route call evidence it did not have when it was written.** Run [735](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/735) — the first time `release.yml` has ever fired — went **red**: 117/117 tests passed and the `.deb` built, then the *Publish to Debian registry* step exited 1 at [`scripts/publish-deb.sh:33`](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/523a45587f12298ded00a988318a2c0ce25d2f85/scripts/publish-deb.sh#L33) with `error: no token`, because `secrets.RELEASE_TOKEN` **expanded to empty**. The *Create release and attach .deb* step — the three hand-rolled `curl`s this finding is about — never executed, so `v1.4.0` was published by hand. **This does not decide A vs B**, and it is important that it is not read as doing so: the paragraph above already measured that `scripts/publish-deb.sh` PUTs to a package-registry surface **neither** route touches, so the 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 `curl` calls, recorded 2026-08-30T15:47Z.** The create at [`release.yml` L49-51](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/033a40c7a0b488e32adf18f4977bb50111cb18fc/.forgejo/workflows/release.yml#L49-L51) sends `{"tag_name":"$TAG","name":"$TAG","draft":false,"prerelease":false}` and **no `body`** — and the step reads no file for one — so every release this workflow publishes has an empty notes pane. That closes a loop: [`scripts/build-deb.sh` L60-66](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/033a40c7a0b488e32adf18f4977bb50111cb18fc/scripts/build-deb.sh#L60-L66) writes the `.deb`'s own `changelog.gz` as the single bullet *"See the repository release 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 with `lib/changelog.sh`, this repo has no equivalent, and until #32 gained the task on 2026-08-30 it had no `CHANGELOG.md` at all. **The half of this that pointed at the `curl` calls is spent:** `release create` takes `--body-file <path>` (`src/cli.js` L1147-1148, read by `readBodyOption` L62-71), so *sending* a body is a shipped capability and no longer waits on the conversion question above. What remains is only the *extraction* — `--body-file` reads a whole file verbatim, it does not select a section — so the missing piece is a `CHANGELOG.md` section-extractor, and it 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 sources `lib/changelog.sh`; its matcher is `awk '/^## / && $2 == ver'`. Both files were fetched at tag `0.6.3` from `forgejo.heavyduty.builders` and run against **this repository's `CHANGELOG.md` at `9586d2c`, unmodified**: `changelog-section 1.4.0` → **exit 0**, 21 lines, the complete `### Added` / `### Changed` / `### Fixed` body on stdout, nothing on stderr. The heading `## 1.4.0 — 2026-08-31` matches because the date lands in `$3`, not `$2`. So the extraction is 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 at all**, because ceremony's release workflow already publishes the version's own `CHANGELOG.md` section 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. - [x] #26 — `issue create --label` (P1) - [x] #24 — `repo create --owner` (P1) — **merged 2026-08-21 as !35; #24 closed** - [x] #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 the `Closes #25` was correct and nothing was owed to `post-merge` - [x] #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 head `2efc76f2` (@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 derived `post-merge` move** — !38 says `Refs #1` and 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 no `v*` 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 is carried 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.0` was pushed at `523a4558` and `release.yml` ran on it as run 735, the first time that workflow had ever fired. It went red 23 seconds in at `scripts/publish-deb.sh` L33 — `error: no token`, because `secrets.RELEASE_TOKEN` expanded to empty — so *Create release and attach `.deb`* never ran, and `stoke 1.4.0` reached the registry and its release page by hand. A second `v*` tag has fired since: `v1.5.0` at `088e7e2d`, 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 runs `release.yml` has ever 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.0` was pushed 2026-07-26T21:37:32Z, long before this row existed. The source claim on the issue this row is about states the precise thing instead — that `v1.3.0` predates the workflow, 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.0` is tagged by @andres and `release.yml` runs on it"* — still unchecked. The structure 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 <another issue>"* 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.** - [x] #23 — `repo sync`, spec narrowed per finding 2 (P3) — **merged 2026-08-31 as !41 (`c34a8b04`); #23 closed the same second by its `Closes #23`**. Built by @codex-bot-andresmgsl, claimed 2026-08-31T16:41Z. The panel approved at the merged head `316bec58` (@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 the `Closes` was correct and nothing was owed to `post-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.** - [x] #54 — `.forgejo/workflows/release.yml`: convert the three hand-rolled `curl`s to stoke's own CLI and publish the version's `CHANGELOG.md` section 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/55), opened 09:19:44Z, whose body opens `Refs #54` — so the close is triage's and not the merge's. **Merged into `main` 2026-09-02T11:22:58Z as `d6a21c9d`**, by @andres, at head `ee88d7d3`. The sweep derived the transition at 11:37:30Z — `claimed` removed, the assignee released, `post-merge` added — so the live labels are now `enhancement`+`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 one surviving criterion (AC6) was discharged by run [1106](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/1106), 2026-09-03T20:52:30Z: the converted door published `v1.5.0` end to end — release id **66**, `stoke_1.5.0_all.deb` attached at 20:52:44Z, and the notes pane carrying the 713-byte `1.5.0` `CHANGELOG.md` section produced by `changelog-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 is `enhancement`+`post-merge`+`scope:ci`+`scope:packaging`. Nothing in the transition record above 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_references` hands each whole row to the state check, so any `#N` in a row counts as a child). The sweep nudged 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 one operator-gated criterion. **Half of that gate is now spent, 2026-09-02T19:33:47Z: @andres set `RELEASE_TOKEN` at the heavy-duty org level, so the criterion waits only on the next `v*` tag.** **That half is spent too, and it did **not** discharge the gate (triage, 2026-09-02): `v1.5.0` was pushed at `088e7e2d` on 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 carries `read: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-jobs` and `…/actions/runs/990/jobs/0/rerun` all return **404** (probed 2026-09-03T21:1xZ). Triage wrote that instruction onto four bodies — this one, [#54](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/54), [#56](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/56), [#60](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/1104) (`failure`, missing `write:repository`) and run [1106](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/1106) (`success`), both `push` events on `v1.5.0` at `088e7e2d`. It was harmless: `refs/tags/v1.5.0` still resolves to the same annotated tag object `6ba87b48acff896d47065a24b0cde7fe3c0d3373` at the same commit with the same tagger and date, and `publish-deb.sh`'s `409` arm 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/56), release `1.5.0`, whose PR [!58](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/58) merged into `main` as `088e7e2d`. Its five pre-merge criteria are verified at that commit, the sweep derived its transition at 20:47:58Z, and it now reads `post-merge` and unassigned with only its three tag-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.0` resolves to `088e7e2d` and `release.yml` ran — and the third criterion's registry half is met as well: `GET /api/v1/packages/heavy-duty?type=debian` returns `debian stoke 1.5.0`, created 22:29:42Z. What is left is the release page carrying the `CHANGELOG.md` section as its notes with `stoke_1.5.0_all.deb` attached (`GET /releases/tags/v1.5.0` → **404**, re-measured this tick), and a clean-container install. Both wait on the re-run: run 990 is `failure`, and it died at *Create release and attach `.deb`* on `403 … required scope(s): [read:user]` **after** the registry step had already returned `201`. #54's AC6 shares that identical wake, so the two still move together — which is the only part of this sentence that survived the tag. (This paragraph said `ready` and unassigned 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 `labeled` event'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; the sixth row and this paragraph revised 2026-09-02. ## Acceptance criteria - [x] Every issue above is either closed or carries a body that reflects the findings here. - [x] No workflow in this org needs a hand-rolled `curl` with a pasted token to create an org repo, label an issue, or attach a release asset. **Ticked 2026-09-02 on !55's merge (`d6a21c9d`), which deleted the last one.** The CLI could do all three as of `033a40c` (2026-08-30), but `release.yml` L47-55 went on hand-rolling the asset attach until that merge; 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`/`lib` file in all **9** repositories of `heavy-duty` on **forgejo.heavyduty.builders** was scanned — 91 files, matching `curl` with 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.yml` L47/49/53** — in scope, 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/stoke` `scripts/publish-deb.sh` L38, a `PUT` to `/api/packages/heavy-duty/debian/pool/…` — a package-registry surface that is none of the three and for which `src/cli.js` has **no command group at all** (no `package` command; the file's only registry token is the `read:package`/`write:package` scope string at L154); `heavy-duty/ceremony` `.github/workflows/labels.yml` L175/L190, a default-branch read and the sweep dispatch, on the ambient `GITHUB_TOKEN` and not a pasted secret; and `heavy-duty/ceremony` `lib/forge-forgejo.sh` L143/160/532/664/851, the engine's own forge transport — 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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-equipment` and `stoke` have default-branch commits 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 **one** token-bearing `curl`, `scripts/publish-deb.sh` L38 — the out-of-scope registry `PUT` — and `.forgejo/workflows/release.yml` no longer appears at all; `office-equipment` (`82e32a9c`) carries **none**; `ceremony` (`91aee7f8`) carries the same two out-of-scope files as before, though their line numbers have drifted with the tree (`lib/forge-forgejo.sh` now L143/160/532/536/664/855, recorded here on 2026-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 L38` citations in this criterion — the out-of-scope list above and the re-measurement here — are spent at `main`. Triage, 2026-09-03; kept verbatim, and noted here because this is the paragraph that corrected a *sibling* repo's drifted line numbers and has since acquired its own.)** [!59](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/59) merged as [`bbde478`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/bbde478157b839eb421577dc19f51c9aabe31791) at 2026-09-03T07:30:12Z, replacing `publish-deb.sh`'s one-line empty-token guard with a six-line heredoc and pushing everything below it down by **8** lines. The token-bearing `PUT` is **L38-40** at `d6a21c9d` and at `088e7e2d` (52 lines) and **L46-48** at `2230ca25` (60 lines), with the `Authorization: token` header on **L47**. **The measurement is untouched — only the two `L38`s moved:** at `main`, `grep -c 'Authorization: token' scripts/publish-deb.sh` is still **1**, it is still the package-registry `PUT`, and it is still out of scope. Re-resolved in the same pass and **unchanged**: `src/cli.js` L154 is still the `read:package`/`write:package` scope string, and `heavy-duty/ceremony` is still at `91aee7f8` with `lib/forge-forgejo.sh` curls at L143/160/532/536/664/855 exactly as corrected above. **Host note, same trap as `heavy-duty/ceremony` on #36:** `heavy-duty` is itself two organizations — **9** repositories on `forgejo.heavyduty.builders` against **28** on `github.com`, sharing five names (`box`, `rig`, `cast`, `ceremony`, `crew`) — and `stoke` exists 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. - [x] `apt-get install stoke` on a clean container verifies signatures via `signed-by=`, and no `[trusted=yes]` is written by any path the user did not 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 `InRelease` is authentically signed but unparseable — the documented 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 — Ubuntu 24.04 installs with the `signed-by=` source retained, Debian 13 refuses and leaves no source behind, and the opt-in is the only route to `[trusted=yes]`. Two honest limits on this tick, neither of which contradicts it: those transcripts were taken at `9cc9576`, one commit before the merged head, and the delta at `2efc76f` only **narrows** the opt-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 25519` is empty and the single `[trusted=yes]` write at `scripts/install-apt.sh:164` sits behind both the exact `/usr/bin/sqv` + `Malformed MPI` condition and `STOKE_ALLOW_UNVERIFIED_APT=1`. - [x] #23 states in its own body which of the three shapes (fast-forward 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 an asset 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`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/branch/main/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 `main` rather than the 2026-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.
Author
Member

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:

  • Core labels bootstrapped — the ceremony core set (queue, PR-state, blocker, signal, and type labels) mirrored byte-for-byte from heavy-duty/ceremony's definitions. No scope:* 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.
  • Queue states set: this epic → epic; #26, #24, #25ready; #23ready after its spec revision (finding 2); #1blocked (by PR #21) + needs-ruling (anonymous registry access — escalation on #1).
  • Bodies of #1 and #23 revised per findings 1 and 2, so both now meet the issue contract as specced; the epic's task list heading was normalized to ## Task list for sweep visibility.

Open PR #21 is untouched — it is the reviewers' surface, and it now gates #1.

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: - **Core labels bootstrapped** — the ceremony core set (queue, PR-state, blocker, signal, and type labels) mirrored byte-for-byte from `heavy-duty/ceremony`'s definitions. No `scope:*` 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. - **Queue states set**: this epic → `epic`; #26, #24, #25 → `ready`; #23 → `ready` after its spec revision (finding 2); #1 → `blocked` (by PR #21) + `needs-ruling` (anonymous registry access — escalation on #1). - **Bodies of #1 and #23 revised** per findings 1 and 2, so both now meet the issue contract as specced; the epic's task list heading was normalized to `## Task list` for sweep visibility. Open PR #21 is untouched — it is the reviewers' surface, and it now gates #1.
claude-bot-andresmgsl added the
epic
label 2026-08-18 00:23:51 +00:00
Author
Member

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:

  • "Every issue above is either closed or carries a body that reflects the findings here."#1 was rescoped to the [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.
  • "#23 states in its own body which of the three shapes (fast-forward sync / documented merge / pull-mirror) it implements, and which it explicitly does not."#23's spec carries an Explicitly out of scope block naming all three: diverged trees refused (procedure owned by ceremony#200), Forgejo pull-mirrors declared a forge-side per-repo setting rather than a CLI verb, release mirroring left composable from 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:

child claim measured on main still true
#26 issue create cannot set labels --label absent; PR !29 carries it, unmerged yes — claimed, PR open, approved 2 of 3
#24 repo create has no --owner src/cli.js L424–432: name, description, private/public, auto-init, default-branch — no owner yes — ready
#25 release create cannot attach assets src/cli.js L1052–1061: tag, target, title, body, draft, prerelease — no --asset yes — ready
#23 no repo sync no sync subcommand exists yes — ready
#1 [trusted=yes] gap unclosed blocked on !21, which is red since 2026-07-28 yes — blocked

Task 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 — 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:** - *"Every issue above is either closed or carries a body that reflects the findings here."* — #1 was rescoped to the `[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. - *"#23 states in its own body which of the three shapes (fast-forward sync / documented merge / pull-mirror) it implements, and which it explicitly does not."* — #23's spec carries an **Explicitly out of scope** block naming all three: diverged trees refused (procedure owned by ceremony#200), Forgejo pull-mirrors declared a forge-side per-repo setting rather than a CLI verb, release mirroring left composable from `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: | child | claim | measured on `main` | still true | |---|---|---|---| | #26 | `issue create` cannot set labels | `--label` absent; PR !29 carries it, unmerged | yes — `claimed`, PR open, approved 2 of 3 | | #24 | `repo create` has no `--owner` | `src/cli.js` L424–432: name, description, private/public, auto-init, default-branch — no owner | yes — `ready` | | #25 | `release create` cannot attach assets | `src/cli.js` L1052–1061: tag, target, title, body, draft, prerelease — no `--asset` | yes — `ready` | | #23 | no `repo sync` | no `sync` subcommand exists | yes — `ready` | | #1 | `[trusted=yes]` gap unclosed | blocked on !21, which is red since 2026-07-28 | yes — `blocked` | Task 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.
Author
Member

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 ready on 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:

  • #24 had no tasks, no checkboxed acceptance criteria and no Part of #27 edge, though its fix was already decided. Retitled to name the deliverable.
  • #25 still listed an optional item — the release id — that this epic had already folded into its scope. Now decided, along with --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 with blocker:ci-red and a standing REQUEST_CHANGES at head 33079afb, and #1's own first task is that it lands — but a false claim in its body was corrected: the instance does have a registered docker runner, so its last unmet original criterion wakes on a v* 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.

**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 `ready` on 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: - **#24** had no tasks, no checkboxed acceptance criteria and no `Part of #27` edge, though its fix was already decided. Retitled to name the deliverable. - **#25** still listed an *optional* item — the release id — that this epic had already folded into its scope. Now decided, along with `--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 with `blocker:ci-red` and a standing `REQUEST_CHANGES` at head `33079afb`, and #1's own first task is that it lands — but a false claim in its body was corrected: the instance *does* have a registered `docker` runner, so its last unmet original criterion wakes on a `v*` 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.
claude-bot-andresmgsl changed title from epic: the five open CLI gaps — priority order, and two that need rescoping before they are buildable to epic: the five CLI gaps — priority order, and the two spec rescopes (#1, #23) that made them buildable 2026-08-20 06:29:02 +00:00
Author
Member

Triage — 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:

  • the priority table told a builder "Rescope before building" (#1) and "Revise the spec before building" (#23);
  • the task list read "rescope to the verification gap, then close it" (#1) and "revise the spec per finding 2, then build the narrowed verb" (#23).

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:

  • Both table cells now record the rescope as landed (dated, still pointing at the finding that drove it) instead of ordering it.
  • Task list now names the remaining work — build #1, build #23 — and surfaces that #1 is on !21, which the checklist had never shown.
  • Title carried the same false claim ("two that need rescoping before they are buildable") and a count that went stale when #26 merged as !29 and closed; it now names what the epic delivered.

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 — 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: - the **priority table** told a builder "Rescope before building" (#1) and "Revise the spec before building" (#23); - the **task list** read "rescope to the verification gap, then close it" (#1) and "revise the spec per finding 2, then build the narrowed verb" (#23). 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: - Both table cells now record the rescope as landed (dated, still pointing at the finding that drove it) instead of ordering it. - Task list now names the remaining work — build #1, build #23 — and surfaces that **#1 is on !21**, which the checklist had never shown. - **Title** carried the same false claim ("two that need rescoping before they are buildable") and a count that went stale when #26 merged as !29 and closed; it now names what the epic delivered. 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.
Author
Member

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 main c2ef6a2 this tick). Dependencies now names
the 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.md
states 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, no
label event since — correctly carries no queue label). Task list re-verified
current: #26 ticked and closed, #24/#25/#1/#23 open, #1 still blocked on !21.

**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`](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/commit/c2ef6a2/docs/UPSTREAM-SYNC.md) (verified present on ceremony `main` `c2ef6a2` this tick). Dependencies now names the 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.md` states 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, no label event since — correctly carries no queue label). Task list re-verified current: #26 ticked and closed, #24/#25/#1/#23 open, #1 still `blocked` on !21.
Author
Member

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 single
1653-line file carrying every command group), src/api.js, and README.md — and all three
carried ready from 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 ready as the single source; #25 and #23 moved to blocked with ordering
edges 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}, and
this epic → {} (finding 3's prose is deliberately written so the marker phrase does not
parse — an epic must not declare a gate it does not have).

**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 single 1653-line file carrying every command group), `src/api.js`, and `README.md` — and all three carried `ready` from 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 `ready` as the single source; #25 and #23 moved to `blocked` with ordering edges 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}`, and this epic → `{}` (finding 3's prose is deliberately written so the marker phrase does not parse — an epic must not declare a gate it does not have).
Author
Member

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 an
over-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) and release 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 — 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 an over-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](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/branch/main/TRIAGE.md#the-issue-contract) (#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) and `release 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.
claude-bot-andresmgsl added the
scope:cli
label 2026-08-21 06:40:51 +00:00
Author
Member

Triage — task list brought current. One child landed, one is claimed, one changed queue state; nothing here needs a decision.

  • #24repo create --owner: ticked. Merged as !35 at 2026-08-21T23:18:38Z (c09943e), #24 closed the same minute. repo create on main now takes the owner. That is the second of the five gaps closed, after #26.
  • #25release create --asset: claimed 2026-08-30 by @codex-bot-andresmgsl; !37 is open at state:addressing. Left unticked — the checklist tracks landings, not claims — but noted in the row so a scan does not read it as untouched.
  • #1 — no longer 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.
  • #23 unchanged: 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 — task list brought current. One child landed, one is claimed, one changed queue state; nothing here needs a decision.** - **#24 — `repo create --owner`: ticked.** Merged as !35 at 2026-08-21T23:18:38Z (`c09943e`), #24 closed the same minute. `repo create` on `main` now takes the owner. That is the second of the five gaps closed, after #26. - **#25 — `release create --asset`: claimed 2026-08-30** by @codex-bot-andresmgsl; !37 is open at `state:addressing`. Left unticked — the checklist tracks landings, not claims — but noted in the row so a scan does not read it as untouched. - **#1 — no longer `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. - **#23** unchanged: `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.
Author
Member

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

body said measured now
this epic, priority table (P2 row for #1) "with !38 open as a draft" draft until 11:04Z; open and undrafted since
this epic, task list (#1 row) "!38 open as a draft" + "!38 may leave draft" left draft; the forward-looking clause is spent
#1, Dependencies "!38 may leave draft" same clause, same rot
#32, Spec item 3 "!38#1's PR, state:building" state:addressing since 11:13:51Z

The #32 one is the one that pointed a reader the wrong way. That paragraph exists to decide whether !38's changelog.d/1.md lands before #32's branch point, and state:building reads as still being written. It is state:addressing at 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:

reviewer verdict at
@glm-bot-andresmgsl APPROVED 11:10:15Z
@claude-bot-andresmgsl REQUEST_CHANGES 11:13:13Z
@kimi-bot-andresmgsl APPROVED 11:13:42Z

Round complete, not fully approved ⇒ ordinary builder-owed fix. state:addressing was set at 11:13:51Z and forgejo-actions cleared state:bots-reviewing 19 s later, so the label engine and the board agree. No attention is 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

  • blockedready: nothing to flip. Zero blocked anywhere at state=all.
  • Reclaims: #1 is the only live claimed; open PR, activity minutes old.
  • Obsolete: none. All four ready issues re-verified as still-real gaps at 25c7267e#23 (no sync in repo), #32 (v1.3.0..main = 39, 10 merges, only tag v1.3.0), #33 (12 kimi-reviewer-andresmgsl hits in README.md), #36 (all three remaining items live).
  • #36's own body, which still reads "codex is building !38 against the 0.6.1 copy 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 and BUILDER.md at 0.6.3 is what codex reads now. A dated measurement ages; it does not rot.
  • #1's ruling record, which contains the same "!38 may leave draft" words. That is a statement of what the ruling unblocked, in a dated decision block. Only the undated present-tense copy in Dependencies was touched.
  • Acceptance criterion 2 here stays unticked: main has not moved since 25c7267e, so .forgejo/workflows/release.yml L47-55 still hand-rolls the asset attach.
**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 | body | said | measured now | |---|---|---| | this epic, priority table (P2 row for #1) | *"with !38 open as a draft"* | draft until 11:04Z; open and undrafted since | | this epic, task list (#1 row) | *"!38 open as a draft"* + *"!38 may leave draft"* | left draft; the forward-looking clause is spent | | #1, Dependencies | *"!38 may leave draft"* | same clause, same rot | | #32, Spec item 3 | *"!38 — #1's PR, `state:building`"* | `state:addressing` since 11:13:51Z | The #32 one is the one that pointed a reader the wrong way. That paragraph exists to decide whether !38's `changelog.d/1.md` lands before #32's branch point, and `state:building` reads as *still being written*. It is `state:addressing` at 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: | reviewer | verdict | at | |---|---|---| | @glm-bot-andresmgsl | APPROVED | 11:10:15Z | | @claude-bot-andresmgsl | REQUEST_CHANGES | 11:13:13Z | | @kimi-bot-andresmgsl | APPROVED | 11:13:42Z | Round complete, not fully approved ⇒ ordinary builder-owed fix. `state:addressing` was set at 11:13:51Z and `forgejo-actions` cleared `state:bots-reviewing` 19 s later, so the label engine and the board agree. **No `attention` is 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. Zero `blocked` anywhere at `state=all`. - **Reclaims:** #1 is the only live `claimed`; open PR, activity minutes old. - **Obsolete:** none. All four `ready` issues re-verified as still-real gaps at `25c7267e` — #23 (no `sync` in `repo`), #32 (`v1.3.0..main` = 39, 10 merges, only tag `v1.3.0`), #33 (12 `kimi-reviewer-andresmgsl` hits in `README.md`), #36 (all three remaining items live). - **#36's own body**, which still reads *"codex is building !38 against the `0.6.1` copy 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 and `BUILDER.md` at `0.6.3` is what codex reads now. A dated measurement ages; it does not rot. - **#1's ruling record**, which contains the same *"!38 may leave draft"* words. That is a statement of what the ruling unblocked, in a dated decision block. Only the undated present-tense copy in Dependencies was touched. - Acceptance criterion 2 here stays unticked: `main` has not moved since `25c7267e`, so `.forgejo/workflows/release.yml` L47-55 still hand-rolls the asset attach.
Author
Member

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.1 doctrine 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 (125e44a25c7267, recorded on #36) moved .ceremony/ to ceremony 0.6.3, and 0.6.3's RELEASES.md adds a ## The membership record section — now at .ceremony/RELEASES.md L43 in this tree — which makes window stands a machine-read predicate: an open release-labelled issue whose membership record, under a literal ## Members heading, holds at least one open member. It states "there is no fallback to the gate." The matching parser is membership_references() in the engine this repo's callers now pin.

Measured before rewording, not assumed:

check result
open release-labelled issues #32 (ready), #1 (claimed)
## Members heading in either body none#32's headings are Why now / Unreleased inventory / Spec / Tasks / Acceptance criteria / Test plan / Dependencies / Notes; #1's are Context / Spec / Ruling / Tasks / Acceptance criteria / Test plan / Dependencies
release window elsewhere on the open board #27 only (L147, L182); #36's mention is inside a dated 2026-08-30T20:12Z record and correctly ages rather than rots
sweep at the new pin run 524, 13:00Z — HEAD is now at 8f0ef79 (0.6.3), labels: reconciled. / issueflow: reconciled., no window flag, no blocked declarations parse to line

So 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 ## Members record, and writing one is not bookkeeping: it is what makes a window stand, after which every ready issue 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 ## Members section was added to #32 or #1, and none should be. RELEASES.md puts 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-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.1` doctrine 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 ceremony `0.6.3`, and `0.6.3`'s `RELEASES.md` adds a `## The membership record` section — now at `.ceremony/RELEASES.md` L43 in this tree — which makes *window stands* a machine-read predicate: an open `release`-labelled issue whose membership record, under a literal `## Members` heading, holds at least one open member. It states *"there is no fallback to the gate."* The matching parser is `membership_references()` in the engine this repo's callers now pin. **Measured before rewording, not assumed:** | check | result | |---|---| | open `release`-labelled issues | **#32** (`ready`), **#1** (`claimed`) | | `## Members` heading in either body | **none** — #32's headings are Why now / Unreleased inventory / Spec / Tasks / Acceptance criteria / Test plan / Dependencies / Notes; #1's are Context / Spec / Ruling / Tasks / Acceptance criteria / Test plan / Dependencies | | `release window` elsewhere on the open board | #27 only (L147, L182); #36's mention is inside a dated 2026-08-30T20:12Z record and correctly ages rather than rots | | sweep at the new pin | run 524, 13:00Z — `HEAD is now at 8f0ef79` (`0.6.3`), `labels: reconciled.` / `issueflow: reconciled.`, no window flag, no `blocked declarations parse to` line | So **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 `## Members` record, and writing one is not bookkeeping: it is what *makes* a window stand, after which every `ready` issue 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 `## Members` section was added to #32 or #1, and none should be. `RELEASES.md` puts 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.*
Author
Member

Triage, 2026-08-31T16:41Z — #1 landed; epic updated. Tick record for the whole board.

!38 merged 16:05:04Z (fb5cb474, head 2efc76f2; @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 says Refs #1 and no commit in the merged range carries a close keyword, so the derived claimedpost-merge move 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 25519 is empty tree-wide, and the single [trusted=yes] write at scripts/install-apt.sh:164 sits behind both the exact /usr/bin/sqv + Malformed MPI condition and STOKE_ALLOW_UNVERIFIED_APT=1.

The remaining unticked box is #23, and it is a real gap: repo at fb5cb474 still offers list / clone / create / import / rename / import-batch / transfer and no sync. The other unticked criterion — no hand-rolled curl for an org repo, a label, or a release asset — also stays unticked and was re-measured, not assumed: release.yml L47-55 still attaches the .deb with curl -F attachment=@….

Rest of the board, swept this tick. #33 still real (12 kimi-reviewer-andresmgsl hits in README.md at fb5cb474); #36 still real (label id 261 still reads 0.6.1's "owes normalization or conversion to a discussion" — the bootstrap=yes dispatch it waits on has deliberately not been fired); #32 claimed and moving with !40 open. Zero blocked, needs-triage, attention, needs-ruling, post-merge and offsite across 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 a Closes #N, which closes an issue but never releases its claim, and issueflow-reconcile enumerates issues?state=open, so no sweep will ever look at them again. Released on both, and on #1. Their assignees stay as build attribution.

**Triage, 2026-08-31T16:41Z — #1 landed; epic updated. Tick record for the whole board.** !38 merged **16:05:04Z** (`fb5cb474`, head `2efc76f2`; @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 says `Refs #1` and no commit in the merged range carries a close keyword, so the derived `claimed` → `post-merge` move 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 25519` is empty tree-wide, and the single `[trusted=yes]` write at `scripts/install-apt.sh:164` sits behind both the exact `/usr/bin/sqv` + `Malformed MPI` condition and `STOKE_ALLOW_UNVERIFIED_APT=1`. **The remaining unticked box is #23, and it is a real gap**: `repo` at `fb5cb474` still offers list / clone / create / import / rename / import-batch / transfer and no `sync`. The other unticked criterion — no hand-rolled `curl` for an org repo, a label, or a release asset — also stays unticked and was re-measured, not assumed: `release.yml` L47-55 still attaches the `.deb` with `curl -F attachment=@…`. **Rest of the board, swept this tick.** #33 still real (12 `kimi-reviewer-andresmgsl` hits in `README.md` at `fb5cb474`); #36 still real (label id 261 still reads `0.6.1`'s *"owes normalization or conversion to a discussion"* — the `bootstrap=yes` dispatch it waits on has deliberately not been fired); #32 claimed and moving with !40 open. Zero `blocked`, `needs-triage`, `attention`, `needs-ruling`, `post-merge` and `offsite` across 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 a `Closes #N`, which closes an issue but never releases its claim, and `issueflow-reconcile` enumerates `issues?state=open`, so no sweep will ever look at them again. Released on both, and on #1. Their assignees stay as build attribution.
Author
Member

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, no claimed, no open PR, no obsolete issue, full 18-issue + 26-PR label audit at
state=all → zero contradictions), so the tick went at the one hold this epic has carried
since 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.yml L47/49/53 hand-roll three curls with secrets.RELEASE_TOKEN at
9586d2c, re-read at that SHA rather than taken from the last tick.

Measured by running the CLI, not reading it (9586d2c, stdin closed):

$ node src/cli.js --config <tmp> auth login --url https://forgejo.heavyduty.builders --token-file …
Authenticated as claude-bot-andresmgsl using provided token.
Credentials stored in <tmp>/config.json                      # exit 0
$ node src/cli.js --config <tmp> release list -o heavy-duty -r stoke
v1.3.0 v1.3.0                                                # exit 0, no prompt

auth login guards both prompts on !token (src/cli.js: if (!username && !token),
if (!password && !token)), and the global --config <path> flag is a preAction hook
setting STOKE_CONFIG_FILE (L24-26), so CI never touches $HOME. No source change is
needed.
release view/create/upload cover the whole hand-rolled block, and release upload --tag resolves the tag to the id itself (getReleaseByTag, L1203), so the RELEASE_ID
plumbing at L47-52 disappears rather than being ported.

Three things the rewrite records that a converter would otherwise trip on:

  1. release.yml L35 already sets STOKE_TOKEN — one step above the curls — and that
    reads like the auth question is already solved in this file. It is not. STOKE_TOKEN is
    honoured by exactly one line in the tree, scripts/publish-deb.sh:30, and nothing under
    src/ reads it
    : src/config.js resolves the token only from a config file. Export it
    and call the CLI and you get an unauthenticated client.
  2. "The checkout makes node src/cli.js available with no install" was falsecli.js
    L3 requires commander and a bare checkout exits 1 with MODULE_NOT_FOUND. Harmless
    here only because L28 runs npm ci first, but it was the clause carrying "probably
    small", so it is corrected rather than dropped.
  3. Half of Finding 4's second hold is spent too. release create --body-file <path>
    exists (L1147-1148 / readBodyOption L62-71), so sending release notes never depended on
    the conversion question. Only extraction of the 1.4.0 section from CHANGELOG.md
    remains, 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.0 is not tagged
yet — #32's post-merge criteria 1-2 wake on exactly that tag push. Rewriting the publisher
while a release is pending is your call.

  • Default, recorded in the body: mint it after v1.4.0 ships. Reversible, and it
    keeps the release path frozen under the tag #32 is waiting on.
  • Alternative: mint it now as a fresh ready issue, accepting that v1.4.0 publishes
    through freshly-changed code.

Nothing is blocked either way, so I have not set needs-ruling — the board would be claiming
a gate that does not exist. One word here and it gets minted.

### 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`, no `claimed`, no open PR, no obsolete issue, full 18-issue + 26-PR label audit at `state=all` → zero contradictions), so the tick went at the one hold this epic has carried since 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.yml` L47/49/53 hand-roll three `curl`s with `secrets.RELEASE_TOKEN` at `9586d2c`, re-read at that SHA rather than taken from the last tick. **Measured by running the CLI, not reading it** (`9586d2c`, stdin closed): ``` $ node src/cli.js --config <tmp> auth login --url https://forgejo.heavyduty.builders --token-file … Authenticated as claude-bot-andresmgsl using provided token. Credentials stored in <tmp>/config.json # exit 0 $ node src/cli.js --config <tmp> release list -o heavy-duty -r stoke v1.3.0 v1.3.0 # exit 0, no prompt ``` `auth login` guards both prompts on `!token` (`src/cli.js`: `if (!username && !token)`, `if (!password && !token)`), and the global `--config <path>` flag is a `preAction` hook setting `STOKE_CONFIG_FILE` (L24-26), so CI never touches `$HOME`. **No source change is needed.** `release view/create/upload` cover the whole hand-rolled block, and `release upload --tag` resolves the tag to the id itself (`getReleaseByTag`, L1203), so the `RELEASE_ID` plumbing at L47-52 disappears rather than being ported. Three things the rewrite records that a converter would otherwise trip on: 1. **`release.yml` L35 already sets `STOKE_TOKEN`** — one step above the `curl`s — and that reads like the auth question is already solved in this file. It is not. `STOKE_TOKEN` is honoured by exactly one line in the tree, `scripts/publish-deb.sh:30`, and **nothing under `src/` reads it**: `src/config.js` resolves the token only from a config *file*. Export it and call the CLI and you get an unauthenticated client. 2. **"The checkout makes `node src/cli.js` available with no install" was false** — `cli.js` L3 requires `commander` and a bare checkout exits 1 with `MODULE_NOT_FOUND`. Harmless *here* only because L28 runs `npm ci` first, but it was the clause carrying "probably small", so it is corrected rather than dropped. 3. **Half of Finding 4's second hold is spent too.** `release create --body-file <path>` exists (L1147-1148 / `readBodyOption` L62-71), so *sending* release notes never depended on the conversion question. Only *extraction* of the 1.4.0 section from `CHANGELOG.md` remains, 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.0` is not tagged yet — #32's post-merge criteria 1-2 wake on exactly that tag push. Rewriting the publisher while a release is pending is your call. - **Default, recorded in the body:** mint it **after `v1.4.0` ships**. Reversible, and it keeps the release path frozen under the tag #32 is waiting on. - **Alternative:** mint it now as a fresh `ready` issue, accepting that `v1.4.0` publishes through freshly-changed code. Nothing is blocked either way, so I have not set `needs-ruling` — the board would be claiming a gate that does not exist. One word here and it gets minted.
Author
Member

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 0 at state=all, 18-issue + 26-PR label audit with
zero 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.yml now, or after v1.4.0?" That assumed one route. There are
two, and the tree already names the second one.

  • Route A — port the three curls to stoke's own CLI. Settled yesterday; needs no source
    change.
  • Route B — stop carrying a bespoke release workflow. heavy-duty/crew and
    heavy-duty/provider-seeker, the org's only other release-shipping repos, hand-roll nothing:
    their release.yml is four lines of
    uses: heavy-duty/ceremony/.github/workflows/release.yml@<tag> (crew 0.6.2,
    provider-seeker 0.6.3). That workflow has zero curl calls, detects this forge from
    GITHUB_API_URL's /api/v1 shape via lib/forge.shforge_select, and runs on the
    ambient github.token instead of a pasted secrets.RELEASE_TOKEN. stoke's own
    .ceremony/README.md L9-14 already records this route
    — and a cost of not taking it:
    "docs-sync --fix cannot run until or unless a future issue adopts the ceremony
    release-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.0 section
remained. I never looked upstream for it. Ceremony ships bin/changelog-section <version> [file]. Fetched at 0.6.3 from forgejo.heavyduty.builders and run against this repo's
CHANGELOG.md at 9586d2c unmodified: changelog-section 1.4.0 → exit 0, 21 lines, the
complete ### Added/### Changed/### Fixed body, empty stderr. Ceremony's release workflow
publishes 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.yml
after 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 retire RELEASE_TOKEN from this file:
scripts/publish-deb.sh PUTs the .deb to /api/packages/heavy-duty/debian/pool/…, which
neither 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-duty
on forgejo.heavyduty.builders (workflows, composite actions, scripts/bin/cli/lib),
matching curl with a four-line window because a line-anchored grep missed one. Four files
carry a token-bearing curl; exactly one does any of the three operations the criterion
names, and it is .forgejo/workflows/release.yml.
The criterion is bounded by that single
file — 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/ceremony on #36: heavy-duty is itself two organizations — 9 repos here, 28 on
github.com, five shared names — and stoke exists 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.0 ships. Nothing on the board is gated on your
answer — which is why I have again not set needs-ruling; v1.4.0 can be tagged today against
the release path exactly as it stands, and #32's criteria are unaffected either way. Say the
word and I mint it as a ready issue 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.

### 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 `0` at `state=all`, 18-issue + 26-PR label audit with zero 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.yml` now, or after `v1.4.0`?" That assumed one route. There are two, and the tree already names the second one.** - **Route A** — port the three `curl`s to stoke's own CLI. Settled yesterday; needs no source change. - **Route B** — stop carrying a bespoke release workflow. `heavy-duty/crew` and `heavy-duty/provider-seeker`, the org's only other release-shipping repos, hand-roll nothing: their `release.yml` is four lines of `uses: heavy-duty/ceremony/.github/workflows/release.yml@<tag>` (crew `0.6.2`, provider-seeker `0.6.3`). That workflow has **zero** `curl` calls, detects this forge from `GITHUB_API_URL`'s `/api/v1` shape via `lib/forge.sh` → `forge_select`, and runs on the ambient `github.token` instead of a pasted `secrets.RELEASE_TOKEN`. **stoke's own `.ceremony/README.md` L9-14 already records this route** — and a cost of not taking it: *"`docs-sync --fix` cannot run until or unless a future issue adopts the ceremony release-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.0` section remained. I never looked upstream for it. Ceremony ships `bin/changelog-section <version> [file]`. Fetched at `0.6.3` from `forgejo.heavyduty.builders` and run against this repo's `CHANGELOG.md` at `9586d2c` **unmodified**: `changelog-section 1.4.0` → exit 0, 21 lines, the complete `### Added`/`### Changed`/`### Fixed` body, empty stderr. Ceremony's release workflow publishes 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.yml` after 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 retire `RELEASE_TOKEN` from this file: `scripts/publish-deb.sh` PUTs the `.deb` to `/api/packages/heavy-duty/debian/pool/…`, which neither 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-duty` on `forgejo.heavyduty.builders` (workflows, composite actions, `scripts`/`bin`/`cli`/`lib`), matching `curl` with a four-line window because a line-anchored grep missed one. Four files carry a token-bearing `curl`; **exactly one does any of the three operations the criterion names, and it is `.forgejo/workflows/release.yml`.** The criterion is bounded by that single file — 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/ceremony` on #36: `heavy-duty` is *itself* two organizations — 9 repos here, 28 on `github.com`, five shared names — and `stoke` exists 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.0` ships. Nothing on the board is gated on your answer — which is why I have again not set `needs-ruling`; `v1.4.0` can be tagged today against the release path exactly as it stands, and #32's criteria are unaffected either way. Say the word and I mint it as a `ready` issue 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.
claude-bot-andresmgsl added the
needs-ruling
label 2026-09-01 08:35:16 +00:00

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.

<!-- ceremony:needs-ruling-bare --> 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](https://github.com/heavy-duty/ceremony/blob/main/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.
Author
Member

🧭 needs-ruling — .forgejo/workflows/release.yml: port its three hand-rolled curls to stoke's own CLI, or replace the file with ceremony's reusable release workflow?
Options: A — port the curls to stoke release view/create/upload and vendor a CHANGELOG.md section extractor B — replace the body with uses: heavy-duty/ceremony/.github/workflows/release.yml@0.6.3 and author the release-artifact hook stoke lacks
Recommend: B, because it deletes the hand-rolled block outright rather than re-expressing it, and closes the empty-notes defect and the docs-sync --fix block 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.0 has 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.0 ships; say otherwise
and it gets minted as a fresh ready issue."
That was a sequencing call and a
route call, and only the sequencing half was ever conditional. v1.4.0 shipped
today
— tagged 08:12:57Z at
523a4558,
apt-cache policy stoke now offers 1.4.0, #32 closed — so the sequencing half has
answered 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 an
escalation 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.yml has ever fired — went red. Read from the job log
(/heavy-duty/stoke/actions/runs/735/jobs/0/logs, 200; the /api/v1 routes 404):
tests 117/117, .deb built, then Publish to Debian registry exited 1 at
publish-deb.sh:33
with error: no token, because secrets.RELEASE_TOKEN expanded to empty. The
Create release and attach .deb step — the three curls this finding is about — never
ran. v1.4.0 was 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.sh PUTs to
/api/packages/heavy-duty/debian/pool/…, a package-registry surface neither route
touches. 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 --json replaces the L47 lookup, release create replaces L49-51
and prints the id, release upload --tag --asset replaces L53-55 and resolves the tag
itself, and auth login --token-file + the global --config authenticate
non-interactively with no source change (run end to end at 9586d2c). Its remaining
cost is the extractor: release create --body-file reads a whole file verbatim, so
someone must vendor or reimplement ceremony's bin/changelog-section (~30 lines) to
select the ## 1.4.0 section. The trap recorded above still applies — STOKE_TOKEN is
honoured by exactly one line in the tree and nothing under src/ reads it.

Option B — adopt the reusable workflow. heavy-duty/crew and
heavy-duty/provider-seeker, the org's only other release-shipping repositories, both
carry a four-line uses: and hand-roll nothing. Its cost is named and real: ceremony's
workflow 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 becomes
nothing at all — that workflow publishes the version's own CHANGELOG.md section
as the release body and refuses to publish a release whose section is missing — and
.ceremony/README.md L9-14 records that docs-sync --fix "cannot run until or unless
a 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 — becomes
true 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_TOKEN on
this repository (or the heavy-duty org) is unset or empty. No agent here can read or
set it — GET /actions/secrets returns 403 user should be the owner of the repo — so
this needs @andres regardless of A or B. Note the log evidence narrows what to check:
because publish-deb.sh aborted before issuing any request, the token's scopes are
untested, 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 tag
whose 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 labeled event.

🧭 needs-ruling — `.forgejo/workflows/release.yml`: port its three hand-rolled `curl`s to stoke's own CLI, or replace the file with ceremony's reusable release workflow? Options: A — port the `curl`s to `stoke release view/create/upload` and vendor a `CHANGELOG.md` section extractor B — replace the body with `uses: heavy-duty/ceremony/.github/workflows/release.yml@0.6.3` and author the `release-artifact` hook stoke lacks Recommend: B, because it deletes the hand-rolled block outright rather than re-expressing it, and closes the empty-notes defect and the `docs-sync --fix` block 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.0` has already shipped. Default: none — hard block. This decides the path that publishes stoke's artifacts, which [LABELS.md](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/tag/0.6.3/LABELS.md) makes a hard block by construction (#50 D13). <details><summary>Analysis</summary> **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.0` ships**; say otherwise and it gets minted as a fresh `ready` issue."* That was a sequencing call **and** a route call, and only the sequencing half was ever conditional. **`v1.4.0` shipped today** — tagged 08:12:57Z at [`523a4558`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/523a45587f12298ded00a988318a2c0ce25d2f85), `apt-cache policy stoke` now offers `1.4.0`, #32 closed — so the sequencing half has answered 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](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/tag/0.6.3/TRIAGE.md) says to set `needs-ruling` *"so the board shows where the human's turn is"*, and an escalation that lives only in a paragraph does not. **The new evidence, and what it does and does not decide.** Run [735](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/735) — the first time `release.yml` has ever fired — went red. Read from the job log (`/heavy-duty/stoke/actions/runs/735/jobs/0/logs`, 200; the `/api/v1` routes 404): tests 117/117, `.deb` built, then *Publish to Debian registry* exited 1 at [`publish-deb.sh:33`](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/523a45587f12298ded00a988318a2c0ce25d2f85/scripts/publish-deb.sh#L33) with `error: no token`, because `secrets.RELEASE_TOKEN` **expanded to empty**. The *Create release and attach .deb* step — the three `curl`s this finding is about — never ran. `v1.4.0` was 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.sh` PUTs to `/api/packages/heavy-duty/debian/pool/…`, a package-registry surface **neither** route touches. 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 --json` replaces the L47 lookup, `release create` replaces L49-51 and prints the id, `release upload --tag --asset` replaces L53-55 and resolves the tag itself, and `auth login --token-file` + the global `--config` authenticate non-interactively with no source change (run end to end at `9586d2c`). Its remaining cost is the extractor: `release create --body-file` reads a whole file verbatim, so someone must vendor or reimplement ceremony's `bin/changelog-section` (~30 lines) to select the `## 1.4.0` section. The trap recorded above still applies — `STOKE_TOKEN` is honoured by exactly one line in the tree and **nothing under `src/` reads it**. **Option B — adopt the reusable workflow.** `heavy-duty/crew` and `heavy-duty/provider-seeker`, the org's only other release-shipping repositories, both carry a four-line `uses:` and hand-roll nothing. Its cost is named and real: ceremony's workflow 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 becomes **nothing at all** — that workflow publishes the version's own `CHANGELOG.md` section as the release body and refuses to publish a release whose section is missing — and `.ceremony/README.md` L9-14 records that `docs-sync --fix` *"cannot run until or unless a 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` — becomes true 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_TOKEN` on this repository (or the `heavy-duty` org) is unset or empty. No agent here can read or set it — `GET /actions/secrets` returns 403 `user should be the owner of the repo` — so this needs @andres regardless of A or B. Note the log evidence narrows what to check: because `publish-deb.sh` aborted before issuing any request, the token's **scopes are untested, 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 tag whose 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 `labeled` event. </details>

Every issue referenced by this epic's task list is closed. Please close the epic or extend its task list.

<!-- issueflow:epic-complete --> Every issue referenced by this epic's task list is closed. Please close the epic or extend its task list.
Author
Member

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 owed

The flag is a 48-second race, and the contract's own wording covers it. Measured:

time event
08:35:16Z needs-ruling applied (labeled event, this issue's timeline)
08:35:21Z sweep run 739 starts
08:35:41Z run 739 posts the bare-flag comment — at that instant it was right, there was no ask yet
08:36:04Z the escalation comment lands (32285, 6005 chars: question, two options, recommendation, blocked-statement, Default: none — hard block)
08:36:15Z sweep run 741 re-reads the issue

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 issueflow output is #27: completed epic nudged and nothing else — no second bare flag. Comment 32280 stands as a historical record of the gap, not as a live ask, and ensure_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 done

The 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 list are 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, because epic_references prints whole matched rows and hands every #N in them to reference_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 read KEEP.

What keeps it open: acceptance criterion 2 — "no workflow in this org needs a hand-rolled curl with a pasted token" — is unticked and honestly so. It is bounded to exactly one file, .forgejo/workflows/release.yml L47/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-ruling on this very issue (32285) — route A (port the three curls 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:

  • @andres rules on A vs B → triage mints the conversion issue that same tick, adds it as the sixth ## Task list row, and clears needs-ruling in the comment that records the ruling; or
  • 2026-09-02T08:35Z (24 h from the labeled event 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.yml no longer hand-rolls the asset attach — and not before.

**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 owed The flag is a 48-second race, and the contract's own wording covers it. Measured: | time | event | |---|---| | 08:35:16Z | `needs-ruling` applied (`labeled` event, this issue's timeline) | | 08:35:21Z | sweep run 739 starts | | 08:35:41Z | run 739 posts the bare-flag comment — at that instant it was right, there was no ask yet | | 08:36:04Z | the escalation comment lands (32285, 6005 chars: question, two options, recommendation, blocked-statement, `Default: none — hard block`) | | 08:36:15Z | sweep run 741 re-reads the issue | 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 `issueflow` output is `#27: completed epic nudged` and nothing else — **no second bare flag**. Comment 32280 stands as a historical record of the gap, not as a live ask, and `ensure_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 done The 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 list` are 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, because `epic_references` prints whole matched rows and hands every `#N` in them to `reference_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 read `KEEP`. **What keeps it open:** acceptance criterion 2 — *"no workflow in this org needs a hand-rolled `curl` with a pasted token"* — is unticked and honestly so. It is bounded to exactly one file, `.forgejo/workflows/release.yml` L47/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-ruling` on this very issue (32285) — route **A** (port the three `curl`s 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: - @andres rules on A vs B → triage mints the conversion issue that same tick, adds it as the sixth `## Task list` row, and clears `needs-ruling` in the comment that records the ruling; **or** - **2026-09-02T08:35Z** (24 h from the `labeled` event 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.yml` no longer hand-rolls the asset attach — and not before.
Author
Member

@andres — the question and the two options in comment 32285 are unchanged, needs-ruling stays on, and the 24 h ladder still runs from the labeled event 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 against heavy-duty/ceremony on this forge at tag 0.6.3 (8f0ef796), .github/workflows/release.yml, 385 lines, byte-identical to the sibling checkout — so this is the tree stoke's uses: would actually resolve, not GitHub's same-named repo.

Recommend: A — port the three curls to stoke's own CLI and vendor a CHANGELOG.md section extractor.

What B actually costs, in the order I found it

  1. B breaks stoke's tag scheme, and that is operator-visible. L341-351, the tag door: if [ "$GITHUB_REF_NAME" != "$ver" ]"tag '…' does not match the tree's version '…' — creating nothing." The scheme is bare X.Y.Z, deliberately (L26: "no 'v' prefix — box's 0.6.0 set the scheme"). stoke ships v1.3.0 and v1.4.0, and .forgejo/workflows/release.yml triggers on tags: ['v*']. Under B the next tag must be 1.5.0, or the door fails loudly and publishes nothing. The blast radius is small but not zero: two published releases already carry the v, three README examples pass --tag v1.x.y, and the .deb version (1.4.0, never prefixed) is unaffected, so apt does not care. This is a scheme decision, which is yours, not a plumbing detail.
  2. B activates a second door stoke does not practise. The merge door (L137) fires on every push to main and expects the family ceremony: a release: X.Y.Z PR carrying the release label, bumping X.Y.Z-devX.Y.Z. stoke's main carries a bare 1.4.0 with no -dev and has no such label in use. It can be narrowed to the tag door alone by omitting branches: [main] from the caller's on: — 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.
  3. B does not delete the file; it deletes the three curls. A caller job that says uses: has no step list, and ceremony's workflow declares exactly one input (version-source) and no secrets: passthrough; the artifact hook is invoked with version alone (L366-372). A composite action cannot read secrets — so scripts/publish-deb.sh and its RELEASE_TOKEN cannot 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.
  4. B moves the .deb build out of node:22-bookworm. Ceremony's jobs hardcode runs-on: ubuntu-latest with no container:; stoke builds in a node container today. ubuntu-latest is proven on this instance (every labels/sweep run uses it), but build-deb.sh needing dpkg-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_create resolves GH_TOKENGITHUB_TOKENFORGEJO_TOKEN — so the release half stops depending on the missing RELEASE_TOKEN entirely (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 from changelog_section, and stoke's ## 1.4.0 — 2026-08-31 heading parses correctly under changelog_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 v scheme, 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, since 033a40c). It ticks acceptance criterion 2 as written — that criterion asks that no workflow need a hand-rolled curl with 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, -dev on main, 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-curl conversion.

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.

### 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-ruling` stays on, and the 24 h ladder still runs from the `labeled` event 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 against `heavy-duty/ceremony` **on this forge** at tag `0.6.3` (`8f0ef796`), `.github/workflows/release.yml`, 385 lines, byte-identical to the sibling checkout — so this is the tree stoke's `uses:` would actually resolve, not GitHub's same-named repo. **Recommend: A** — port the three `curl`s to stoke's own CLI and vendor a `CHANGELOG.md` section extractor. #### What B actually costs, in the order I found it 1. **B breaks stoke's tag scheme, and that is operator-visible.** L341-351, the tag door: `if [ "$GITHUB_REF_NAME" != "$ver" ]` → *"tag '…' does not match the tree's version '…' — creating nothing."* The scheme is bare `X.Y.Z`, deliberately (L26: *"no 'v' prefix — box's 0.6.0 set the scheme"*). stoke ships **`v1.3.0`** and **`v1.4.0`**, and `.forgejo/workflows/release.yml` triggers on `tags: ['v*']`. Under B the next tag must be **`1.5.0`**, or the door fails loudly and publishes nothing. The blast radius is small but not zero: two published releases already carry the `v`, three README examples pass `--tag v1.x.y`, and the `.deb` version (`1.4.0`, never prefixed) is unaffected, so apt does not care. **This is a scheme decision, which is yours, not a plumbing detail.** 2. **B activates a second door stoke does not practise.** The merge door (L137) fires on every push to `main` and expects the family ceremony: a `release: X.Y.Z` PR carrying the `release` label, bumping `X.Y.Z-dev` → `X.Y.Z`. stoke's `main` carries a bare `1.4.0` with no `-dev` and has no such label in use. It can be narrowed to the tag door alone by omitting `branches: [main]` from the caller's `on:` — 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. 3. **B does not delete the file; it deletes the three `curl`s.** A caller job that says `uses:` has no step list, and ceremony's workflow declares exactly one input (`version-source`) and **no `secrets:` passthrough**; the artifact hook is invoked with `version` alone (L366-372). A composite action cannot read `secrets` — so `scripts/publish-deb.sh` and its `RELEASE_TOKEN` **cannot** 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. 4. **B moves the `.deb` build out of `node:22-bookworm`.** Ceremony's jobs hardcode `runs-on: ubuntu-latest` with no `container:`; stoke builds in a node container today. `ubuntu-latest` is proven on this instance (every labels/sweep run uses it), but `build-deb.sh` needing `dpkg-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_create` resolves `GH_TOKEN` → `GITHUB_TOKEN` → `FORGEJO_TOKEN` — so **the release half stops depending on the missing `RELEASE_TOKEN` entirely** (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 from `changelog_section`, and stoke's `## 1.4.0 — 2026-08-31` heading parses correctly under `changelog_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 `v` scheme, 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`, since `033a40c`). It ticks acceptance criterion 2 as written — that criterion asks that no workflow need a **hand-rolled `curl` with 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, `-dev` on `main`, 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-`curl` conversion. **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 — this ruling is 12 hours past its labeled event: the ladder's
12h 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: against
everything 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 labeled clock and do not
reset on activity; this comment fires once per flag episode.

<!-- ceremony:needs-ruling-rung12 --> @claude-bot-andresmgsl — this ruling is 12 hours past its `labeled` event: the ladder's 12h rung ([BUILDER.md — the ruling ask](https://github.com/heavy-duty/ceremony/blob/main/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:` against everything 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 `labeled` clock and do not reset on activity; this comment fires once per flag episode.
Author
Member

The 12h rung, answered by the setter — the Default: still reads none — hard block, and the re-read found one new cost, all of it on B's side

The machine's rung fired at 2026-09-01T21:00:26Z (sweep run 880, schedule, started
21:00:07Z, success, 0 ::error / 0 ::warning, logged as
issueflow: #27: ruling at the 12h rung — commented (the setter re-reads the default)). The
ladder is anchored to the needs-ruling labeled event at 2026-09-01T08:35:16Z — three
label events on this issue ever (epic, scope:cli, needs-ruling), no re-flag, so this is the
first and only episode. The rung's duty is mine as the setter: re-read the Default: against what
has 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 main
after 08:35:16Z — 3ea8eb90 (CONTRIBUTING), 112f9468 + d84062af (.github/labeler.yml,
.github/labels.conf, scripts/check-governance.js, test/governance.test.js), and da43f967,
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.js and
CHANGELOG.md are all untouched in that range. On the other side of the pin, ceremony's main last
moved at 2026-09-01T05:52:26Z (91aee7f8), which is before the flag, and its tag list is
still x-total-count: 8 with 0.6.3 (8f0ef796) on top. #36's two newest defects (9 and 10)
were measured in that window but live in lib/issueflow.sh's post_merge_decision, a path this
board 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 reads
none — hard block (published artifacts, ceremony#50 D12–D13). A hard block cannot fire early
and 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.3 and listed four costs. A fifth was
not 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.3 L385 calls
forge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}" with no existence check
on the tag door, and that function at 0.6.3 POSTs the release with draft:false first and
uploads 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.yml L47-52 is get-or-create
and scripts/publish-deb.sh L42-51 treats 409 as Already published, so a re-run recovers.
Upstream fixed it and the fix is in no tag: heavy-duty/ceremony#271"publish the release
atomically, 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.3 was cut 2026-08-26T20:18:16Z and
git merge-base --is-ancestor 58ab503 0.6.3 is false — the fix lives only on ceremony main
(VERSION = 0.6.4-dev). All of it measured against forgejo.heavyduty.builders, not GitHub's
same-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 is
exactly 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.4 carrying #271 erases it, and #36's last two
criteria are already watching for that tag.

Recommendation unchanged: A (comment 32351) — port the three curls to stoke's own CLI and
vendor 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 labeled event, where
the 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-ruling cleared 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.

### The 12h rung, answered by the setter — the `Default:` still reads `none — hard block`, and the re-read found one new cost, all of it on B's side The machine's rung fired at **2026-09-01T21:00:26Z** (sweep run **880**, `schedule`, started 21:00:07Z, `success`, 0 `::error` / 0 `::warning`, logged as `issueflow: #27: ruling at the 12h rung — commented (the setter re-reads the default)`). The ladder is anchored to the `needs-ruling` **`labeled`** event at **2026-09-01T08:35:16Z** — three label events on this issue ever (`epic`, `scope:cli`, `needs-ruling`), no re-flag, so this is the first and only episode. The rung's duty is mine as the setter: re-read the `Default:` against what has 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 `main` after 08:35:16Z — `3ea8eb90` (CONTRIBUTING), `112f9468` + `d84062af` (`.github/labeler.yml`, `.github/labels.conf`, `scripts/check-governance.js`, `test/governance.test.js`), and `da43f967`, 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.js` and `CHANGELOG.md` are all untouched in that range. On the other side of the pin, ceremony's `main` last moved at **2026-09-01T05:52:26Z** (`91aee7f8`), which is *before* the flag, and its tag list is still `x-total-count: 8` with **`0.6.3`** (`8f0ef796`) on top. #36's two newest defects (9 and 10) were measured in that window but live in `lib/issueflow.sh`'s `post_merge_decision`, a path this board 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 reads **`none — hard block`** (published artifacts, ceremony#50 D12–D13). A hard block cannot fire early and 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.3` and listed four costs. A fifth was not 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.3` L385 calls `forge_release_create "$VER" "$VER" "$RUNNER_TEMP/notes.md" "${assets[@]}"` with no existence check on the tag door, and that function at `0.6.3` POSTs the release with `draft:false` **first** and uploads 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 `curl`s are better on that one axis: `release.yml` L47-52 is get-or-create and `scripts/publish-deb.sh` L42-51 treats **409** as `Already published`, so a re-run recovers. **Upstream fixed it and the fix is in no tag:** `heavy-duty/ceremony#271` — *"publish the release atomically, 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.3` was cut **2026-08-26T20:18:16Z** and `git merge-base --is-ancestor 58ab503 0.6.3` is **false** — the fix lives only on ceremony `main` (`VERSION` = `0.6.4-dev`). All of it measured against `forgejo.heavyduty.builders`, not GitHub's same-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 is exactly 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.4` carrying #271 erases it, and #36's last two criteria are already watching for that tag. **Recommendation unchanged: A** (comment 32351) — port the three `curl`s to stoke's own CLI and vendor 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 `labeled` event, where the 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-ruling` cleared 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 labeled event: the ladder's
24h 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 labeled clock and do not reset on activity; this comment fires once
per flag episode and covers everything past 24h — there is no further
timer.

<!-- ceremony:needs-ruling-rung24 --> @claude-bot-andresmgsl — this ruling is 24 hours past its `labeled` event: the ladder's 24h rung ([BUILDER.md — the ruling ask](https://github.com/heavy-duty/ceremony/blob/main/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 `labeled` clock and do not reset on activity; this comment fires once per flag episode and covers everything past 24h — there is no further timer.
Author
Member

The ruling, decided by triage past the 24h rung: route A. needs-ruling is cleared with this comment, and the work is #54

The rung fired on sweep run 894 (schedule, started 2026-09-02T09:00:05Z, success, 0 ::error / 0 ::warning), logged as issueflow: #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's needs-ruling labeled event, 2026-09-01T08:35:16Z — re-read from the timeline before writing this, and still exactly three label events on this issue ever (epic 2026-08-18T00:23:51Z, scope:cli 2026-08-21T06:40:51Z, needs-ruling 2026-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-rolled curls 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.3 measurement 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.sh against this forge returns c4315c2 "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, and 7bd331a "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 still x-total-count: 8, top 0.6.3 (8f0ef796, 2026-08-26T20:18:16Z), main at 91aee7f8 / VERSION 0.6.4-dev.

2. A sixth cost, and this one is structural — a 0.6.4 does not erase it. Ceremony's tag door asserts the pushed tag equals the tree's version verbatim: release.yml@0.6.3 L347 is if [ "$GITHUB_REF_NAME" != "$ver" ]"does not match the tree's version — creating nothing", exit 1. Every release tag stoke has ever pushed is v-prefixed (v1.3.0, v1.4.0), its workflow filters tags: ['v*'], package.json holds 1.4.0, and there is no VERSION file. Under B, v1.5.0 is 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.yml is still 404 on stoke and 200 on both siblings (re-fetched this tick; crew pins 0.6.2, provider-seeker 0.6.3), so B still means authoring one that runs scripts/build-deb.sh. And B still does not retire RELEASE_TOKEN: scripts/publish-deb.sh PUTs 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 967efa02 while writing #54 — not read, run: auth login --token-file with stdin closed → exit 0; release view --tag v1.4.0 --json → exit 0, release id 63; release view --tag v9.9.9HTTP status: 404, exit 1, which is the get-or-create branch working. The notes extractor is no longer unspecced under A either: ceremony's bin/changelog-section + lib/changelog.sh at 0.6.3, run unmodified against this repository's CHANGELOG.md@967efa02, gave 1.4.0 → exit 0 and 21 lines, 1.5.0 → exit 1 with no section for '1.5.0' on stderr and nothing on stdout. It is six lines of awk to vendor.

5. A keeps the one thing this file is genuinely better at. L47-52 is get-or-create and publish-deb.sh treats 409 as Already published, so a re-run recovers. At 0.6.3 B'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

  • This is a dated decision on cost 1, not a permanent one. A ceremony 0.6.4 carrying c4315c2 + 734676a + 7bd331a erases 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, and docs-sync --fix unblocked (.ceremony/README.md L9-14). Its wake condition is already on the board: it is #36's re-pin, which waits on the same 0.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.
  • A leaves RELEASE_TOKEN in 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.
  • The secret itself is still an operator act and is unaffected by this ruling. Run 735 died at scripts/publish-deb.sh:33 with error: no token because secrets.RELEASE_TOKEN expanded to empty, and release.yml is byte-identical at v1.4.0 and main, so v1.5.0 fails 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

  • Records the decision above, as triage's pick past the 24h rung, with triage accountable.
  • Mints the work: #54".forgejo/workflows/release.yml — publish the release through stoke's own CLI, with the CHANGELOG section as the notes", 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.
  • Adds the sixth epic row, which by this body's own terms could not exist before the route did, and corrects the two prose statements the pick made stale — the "triage does not pick between them" clause and the "no sixth row can exist" paragraph. It also corrects this body's own approximate 08:40Z: the labeled event is 08:35:16Z, and that is what the ladder anchors to.
  • Removes needs-ruling and returns this epic to its flow. It stays open as an epic, now waiting on #54 rather than on a human, and closes when acceptance criterion 2 ticks.
  • Sets no 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_decision now sees an OPEN state and returns KEEP, 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.

### The ruling, decided by triage past the 24h rung: **route A**. `needs-ruling` is cleared with this comment, and the work is #54 The rung fired on sweep run **894** (`schedule`, started 2026-09-02T09:00:05Z, `success`, 0 `::error` / 0 `::warning`), logged as `issueflow: #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's `needs-ruling` **`labeled`** event, **2026-09-01T08:35:16Z** — re-read from the timeline before writing this, and still exactly **three** label events on this issue ever (`epic` 2026-08-18T00:23:51Z, `scope:cli` 2026-08-21T06:40:51Z, `needs-ruling` 2026-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](https://github.com/heavy-duty/ceremony/blob/main/BUILDER.md#the-ruling-ask) and [TRIAGE.md outcome 3](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/branch/main/TRIAGE.md), 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-rolled `curl`s 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.3` measurement 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.sh` against this forge returns **`c4315c2`** *"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**, and **`7bd331a`** *"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 still `x-total-count: 8`, top `0.6.3` (`8f0ef796`, 2026-08-26T20:18:16Z), `main` at `91aee7f8` / `VERSION` `0.6.4-dev`. **2. A sixth cost, and this one is structural — a `0.6.4` does not erase it.** Ceremony's tag door asserts the pushed tag equals the tree's version *verbatim*: [`release.yml@0.6.3` L347](https://forgejo.heavyduty.builders/heavy-duty/ceremony/src/tag/0.6.3/.github/workflows/release.yml#L347) is `if [ "$GITHUB_REF_NAME" != "$ver" ]` → *"does not match the tree's version — creating nothing"*, exit 1. Every release tag stoke has ever pushed is `v`-prefixed (`v1.3.0`, `v1.4.0`), its workflow filters `tags: ['v*']`, `package.json` holds `1.4.0`, and there is no `VERSION` file. Under B, `v1.5.0` is 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.yml` is still **404** on stoke and **200** on both siblings (re-fetched this tick; crew pins `0.6.2`, provider-seeker `0.6.3`), so B still means authoring one that runs `scripts/build-deb.sh`. And B still does not retire `RELEASE_TOKEN`: `scripts/publish-deb.sh` `PUT`s 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 `967efa02` while writing #54** — not read, run: `auth login --token-file` with 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's `bin/changelog-section` + `lib/changelog.sh` at `0.6.3`, run unmodified against **this repository's `CHANGELOG.md@967efa02`**, gave `1.4.0` → exit 0 and 21 lines, `1.5.0` → exit 1 with `no section for '1.5.0'` on stderr and nothing on stdout. It is six lines of `awk` to vendor. **5. A keeps the one thing this file is genuinely better at.** L47-52 is get-or-create and `publish-deb.sh` treats **409** as `Already published`, so a re-run recovers. At `0.6.3` B'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 - **This is a dated decision on cost 1, not a permanent one.** A ceremony `0.6.4` carrying `c4315c2` + `734676a` + `7bd331a` erases 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, and `docs-sync --fix` unblocked ([`.ceremony/README.md` L9-14](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/967efa027cb22d429b9350b71091c4caa5bf45ee/.ceremony/README.md#L9-L14)). **Its wake condition is already on the board: it is #36's re-pin, which waits on the same `0.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. - **A leaves `RELEASE_TOKEN` in 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. - **The secret itself is still an operator act and is unaffected by this ruling.** Run [735](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/735) died at `scripts/publish-deb.sh:33` with `error: no token` because `secrets.RELEASE_TOKEN` expanded to empty, and `release.yml` is byte-identical at `v1.4.0` and `main`, so `v1.5.0` fails 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 - **Records the decision** above, as triage's pick past the 24h rung, with triage accountable. - **Mints the work:** #54 — *".forgejo/workflows/release.yml — publish the release through stoke's own CLI, with the CHANGELOG section as the notes"*, `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. - **Adds the sixth epic row**, which by this body's own terms could not exist before the route did, and corrects the two prose statements the pick made stale — the *"triage does not pick between them"* clause and the *"no sixth row can exist"* paragraph. It also corrects this body's own approximate `08:40Z`: the `labeled` event is **08:35:16Z**, and that is what the ladder anchors to. - **Removes `needs-ruling`** and returns this epic to its flow. It stays **open** as an `epic`, now waiting on #54 rather than on a human, and closes when acceptance criterion 2 ticks. - **Sets no `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_decision` now sees an `OPEN` state and returns `KEEP`, 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.
claude-bot-andresmgsl removed the
needs-ruling
label 2026-09-02 09:17:46 +00:00
Author
Member

Epic de-rot: two sentences in this body asserted #54 was ready and unclaimed. It has been claimed since 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_references hands 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):

when event actor
2026-09-02T09:14:16Z enhancement, ready, scope:packaging added @claude-bot-andresmgsl
2026-09-02T09:17:58Z ready removed @codex-bot-andresmgsl
2026-09-02T09:17:59Z claimed added, self-assigned @codex-bot-andresmgsl
2026-09-02T09:19:44Z !55 opened, Refs #54 @codex-bot-andresmgsl

#54's live label set is claimed + enhancement + scope:packaging, assignee @codex-bot-andresmgsl.

What changed in the body: the "It is minted … ready and 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 labels claimed+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.sh at the pinned 0.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, so epic_decision stays KEEP and 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.

**Epic de-rot: two sentences in this body asserted #54 was `ready` and unclaimed. It has been `claimed` since 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_references` hands 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): | when | event | actor | |---|---|---| | 2026-09-02T09:14:16Z | `enhancement`, `ready`, `scope:packaging` added | @claude-bot-andresmgsl | | 2026-09-02T09:17:58Z | `ready` **removed** | @codex-bot-andresmgsl | | 2026-09-02T09:17:59Z | `claimed` added, self-assigned | @codex-bot-andresmgsl | | 2026-09-02T09:19:44Z | [!55](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/55) opened, `Refs #54` | @codex-bot-andresmgsl | #54's live label set is `claimed` + `enhancement` + `scope:packaging`, assignee @codex-bot-andresmgsl. **What changed in the body:** the *"It is minted … `ready` and 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 labels `claimed`+`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.sh` at the pinned `0.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, so `epic_decision` stays `KEEP` and 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.
Author
Member

Acceptance criterion 2 is ticked, and the body is re-aligned to the board. !55 merged into main at 2026-09-02T11:22:58Z as d6a21c9d, 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):

  1. Criterion 2 ticked, and the sentence that said "release.yml L47-55 still hand-rolls the asset attach, so this stays unticked" replaced — it went stale the instant the merge landed.
  2. The sixth task-list row now records the merge, the merge commit and the derived transition (11:37:30Z: claimed off, assignee released, post-merge on). It stays [ ]#54 is open.
  3. The paragraph under the task list, which justified this epic staying open by criterion 2 being unticked. That reason is now spent; the replacement states the narrower one.

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-equipment and stoke have 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 plus scripts/bin/cli/lib, curl matched with a four-line window). stoke@d6a21c9d: one hit, scripts/publish-deb.sh L38, the out-of-scope registry PUTrelease.yml is 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.sh now 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 at post-merge, holding one operator-gated criterion: @andres sets RELEASE_TOKEN, then the first v* 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.3 L400-407) over the patched text: the same seven locals 1 23 24 25 26 32 54, and the same 6 rows / 5 checked / 1 unchecked. !55 is still linked by URL only, so no #55 child was injected, and neither d6a21c9d nor ee88d7d3 can parse as a reference.

Acceptance criterion 2 is ticked, and the body is re-aligned to the board. !55 merged into `main` at 2026-09-02T11:22:58Z as `d6a21c9d`, 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): 1. **Criterion 2 ticked**, and the sentence that said *"`release.yml` L47-55 still hand-rolls the asset attach, so this stays unticked"* replaced — it went stale the instant the merge landed. 2. **The sixth task-list row** now records the merge, the merge commit and the derived transition (11:37:30Z: `claimed` off, assignee released, `post-merge` on). **It stays `[ ]`** — #54 is open. 3. **The paragraph under the task list**, which justified this epic staying open by criterion 2 being unticked. That reason is now spent; the replacement states the narrower one. **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-equipment` and `stoke` have 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 plus `scripts`/`bin`/`cli`/`lib`, `curl` matched with a four-line window). `stoke@d6a21c9d`: **one** hit, `scripts/publish-deb.sh` L38, the out-of-scope registry `PUT` — `release.yml` is 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.sh` now 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/54). **Why this epic is not being closed, though all four of its criteria now read `[x]`.** Its last child #54 is open at `post-merge`, holding one operator-gated criterion: @andres sets `RELEASE_TOKEN`, then the first `v*` 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.3` L400-407) over the patched text: the same seven locals `1 23 24 25 26 32 54`, and the same 6 rows / 5 checked / 1 unchecked. !55 is still linked by URL only, so no `#55` child was injected, and neither `d6a21c9d` nor `ee88d7d3` can parse as a reference.
Author
Member

🔧 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.0 tag "has a vehicle as of this tick — #56, release 1.5.0, minted ready and unassigned". @codex-bot-andresmgsl claimed #56 four minutes later, at 19:52:31Z, and its PR !58 has since merged into main as 088e7e2d (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 reads post-merge and 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 the v1.5.0 tag push at 088e7e2d. 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 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.0` tag *"has a vehicle as of this tick — #56, release `1.5.0`, **minted `ready` and unassigned**"*. @codex-bot-andresmgsl claimed #56 **four minutes later**, at 19:52:31Z, and its PR [!58](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/58) has since merged into `main` as `088e7e2d` (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 reads `post-merge` and 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 the `v1.5.0` tag push at `088e7e2d`. Tags are still **2**. When that tag lands and #54 closes, this epic closes with it; [comment 34121](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/27#issuecomment-34121)'s standing offer to @andres to close it earlier is still unanswered and still his to take.
Author
Member

🔧 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.json holds 1.4.0". At 088e7e2d it holds 1.5.0#56's bump, merged 20:18:58Z. Verified by GET /contents/package.json?ref=088e7e2d200, 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 is v-prefixed, so v1.5.0 is refused by that assert whether the tree says 1.4.0 or 1.5.0. The sixth cost is structural, exactly as the paragraph claims. Only its supporting measurement had drifted.

The VERSION half re-verified rather than assumed. GET /contents/VERSION?ref=088e7e2d404, run against a 200 control on package.json at the same ref — a 404 with no control is not evidence of absence on this instance. Still no VERSION file; 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.

🔧 **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.json` holds `1.4.0`"*. At `088e7e2d` it holds **`1.5.0`** — #56's bump, merged 20:18:58Z. Verified by `GET /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 is **`v`-prefixed**, so `v1.5.0` is refused by that assert whether the tree says `1.4.0` or `1.5.0`. The sixth cost is structural, exactly as the paragraph claims. Only its supporting measurement had drifted. **The `VERSION` half re-verified rather than assumed.** `GET /contents/VERSION?ref=088e7e2d` → **404**, run against a **200** control on `package.json` at the same ref — a 404 with no control is not evidence of absence on this instance. Still no `VERSION` file; 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.
Author
Member

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.0 push at 088e7e2d, 2026-09-02T22:29:15Z:

  1. "the criterion waits only on the next v* tag" — the tag came. #54's criterion still does not
    read, so the sentence now points at a door that has already been walked through.
  2. "only its three tag-gated criteria left"two, and the gate is no longer a tag. #56's first
    post-merge criterion is ticked (the tag resolves to 088e7e2d and release.yml ran).

Re-measured this tick, not carried over:

check result
GET /repos/heavy-duty/stoke/tags v1.5.0088e7e2d, v1.4.0, v1.3.03
run 990 (deb, push on v1.5.0) status: failure, started 22:29:31Z
GET /api/v1/packages/heavy-duty?type=debian debian stoke 1.5.0, created 22:29:42Z
GET /repos/heavy-duty/stoke/releases/tags/v1.5.0 404
any newer run on v1.5.0 none — 990 is still the only one

So the release is half-shipped: the registry step returned 201 and the Create release and attach
.deb
step died on 403 … required scope(s): [read:user]. The old prediction is kept in the body
rather 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 keeps post-merge, and the task list
above is otherwise accurate — #54 remains this epic's only open child and the only row that stays
unchecked.

**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.0` push at `088e7e2d`, 2026-09-02T22:29:15Z: 1. *"the criterion waits only on the next `v*` tag"* — the tag came. #54's criterion still does not read, so the sentence now points at a door that has already been walked through. 2. *"only its three tag-gated criteria left"* — **two**, and the gate is no longer a tag. #56's first post-merge criterion is ticked (the tag resolves to `088e7e2d` and `release.yml` ran). Re-measured this tick, not carried over: | check | result | | --- | --- | | `GET /repos/heavy-duty/stoke/tags` | `v1.5.0` → `088e7e2d`, `v1.4.0`, `v1.3.0` — **3** | | run 990 (`deb`, `push` on `v1.5.0`) | `status: failure`, started 22:29:31Z | | `GET /api/v1/packages/heavy-duty?type=debian` | `debian stoke 1.5.0`, created **22:29:42Z** | | `GET /repos/heavy-duty/stoke/releases/tags/v1.5.0` | **404** | | any newer run on `v1.5.0` | none — 990 is still the only one | So the release is half-shipped: the registry step returned `201` and the *Create release and attach `.deb`* step died on `403 … required scope(s): [read:user]`. The old prediction is **kept in the body** rather 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 keeps `post-merge`, and the task list above is otherwise accurate — #54 remains this epic's only open child and the only row that stays unchecked.
Author
Member

Triage tick — the Context tally said leaving two; all five have been closed since 2026-08-31

Board otherwise static: 6 open issues / 2 open PRs, 28 labels, tags 3
(v1.5.0/v1.4.0/v1.3.0), main 088e7e2d. Newest repo comment before this one was my own
34435 (04:56:20Z). Every open item reads updated_at 2026-09-03T04:56:20Z — that is
cross-reference fan-out from 34435, not movement.

The find — ## Context L4, corrected in place (body 4515646351 B)

The opening paragraph read:

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.

Measured from the forge this tick, not from the thread:

issue state closed_at closed by
#26 closed 2026-08-19T19:15:33Z !29
#24 closed 2026-08-21T23:18:38Z !35
#25 closed 2026-08-30T13:12:37Z !37
#1 closed 2026-08-31T16:05:05Z !38, fb5cb474, merged 16:05:04Z
#23 closed 2026-08-31T18:42:40Z !41, c34a8b04, merged 18:42:40Z

leaving two is leaving zero, and has been for three days. The sentence is kept verbatim and
annotated 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] and
the 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 at
post-merge"
— verified state=open, label present), L355's "Still not minted" (already
kept-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

  1. blocked → ready: blocked is 0 board-wide at state=all. Never armed.
  2. reclaim: #57!59 (Closes), #60!61 (Refs); both open, mergeable: true, both authored
    by their assignee @codex-bot-andresmgsl. Never arms.
  3. obsolete: none. ceremony#276 still open and still ceremony's only open issue.
  4. labels true: all 28 re-checked against the six issues and two PRs; zero flips owed.
  5. epic currency: rows walked against live state — #26/#24/#25/#1/#23 [x] and all five verified
    closed above; #54 [ ] and verified open. #57 and #60 stay deliberately not rows: #57 is a
    shell-script diagnostic and #60 a workflow comment, neither is a CLI gap.

Unchanged, and still the whole board

GET /releases/tags/v1.5.0404; runs with head_branch == v1.5.01 (run 990,
failure); GET /actions/secrets403. One operator act is still owed and it discharges #56
criterion 2, #54 AC6 and #60's post-merge criterion together: @andres mints a new PAT carrying
read:user + write:package + write:repository into org RELEASE_TOKEN, then re-runs run 990's
job.
No re-tag, no hand-created release.

Sweep 1052 (schedule, 05:00:04Z), 255 lines, 0 ::error / 0 ::warning, both
reconciled., 🏁 Job succeeded, and the User '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 — the Context tally said `leaving two`; all five have been closed since 2026-08-31 Board otherwise static: 6 open issues / 2 open PRs, 28 labels, tags 3 (`v1.5.0`/`v1.4.0`/`v1.3.0`), `main` `088e7e2d`. Newest repo comment before this one was my own `34435` (04:56:20Z). Every open item reads `updated_at` `2026-09-03T04:56:20Z` — that is cross-reference fan-out from `34435`, not movement. ### The find — `## Context` L4, corrected in place (body `45156` → `46351` B) The opening paragraph read: > 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**. Measured from the forge this tick, not from the thread: | issue | `state` | `closed_at` | closed by | |---|---|---|---| | #26 | closed | 2026-08-19T19:15:33Z | !29 | | #24 | closed | 2026-08-21T23:18:38Z | !35 | | #25 | closed | 2026-08-30T13:12:37Z | !37 | | **#1** | **closed** | **2026-08-31T16:05:05Z** | **!38, `fb5cb474`, merged 16:05:04Z** | | **#23** | **closed** | **2026-08-31T18:42:40Z** | **!41, `c34a8b04`, merged 18:42:40Z** | `leaving two` is `leaving zero`, and has been for three days. The sentence is **kept verbatim and annotated 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]` and the 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 at `post-merge`"* — verified `state=open`, label present), L355's *"Still not minted"* (already kept-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 1. **blocked → ready:** `blocked` is **0** board-wide at `state=all`. Never armed. 2. **reclaim:** #57 → !59 (`Closes`), #60 → !61 (`Refs`); both open, `mergeable: true`, both authored by their assignee @codex-bot-andresmgsl. Never arms. 3. **obsolete:** none. ceremony#276 still `open` and still ceremony's only open issue. 4. **labels true:** all 28 re-checked against the six issues and two PRs; **zero flips owed**. 5. **epic currency:** rows walked against live state — #26/#24/#25/#1/#23 `[x]` and all five verified `closed` above; #54 `[ ]` and verified `open`. #57 and #60 stay deliberately **not** rows: #57 is a shell-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 with `head_branch == v1.5.0` → **1** (run 990, `failure`); `GET /actions/secrets` → **403**. One operator act is still owed and it discharges #56 criterion 2, #54 AC6 and #60's post-merge criterion together: **@andres mints a new PAT carrying `read:user` + `write:package` + `write:repository` into org `RELEASE_TOKEN`, then re-runs run 990's job.** No re-tag, no hand-created release. Sweep **1052** (`schedule`, 05:00:04Z), 255 lines, **0 `::error` / 0 `::warning`**, both `reconciled.`, `🏁 Job succeeded`, and the `User '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.
Author
Member

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:

Every release tag stoke has ever pushed is v-prefixedv1.3.0, v1.4.0 — and .forgejo/workflows/release.yml L16-17 filters on tags: ['v*'] … Under B, v1.5.0 would be refused by that assert.

The bolded claim is true. The list is short one member. v1.5.0 was pushed at 088e7e2d on 2026-09-02T22:29:15Z by @claude-lead-andresmgsl (annotated tag 6ba87b48, message stoke 1.5.0); GET /repos/heavy-duty/stoke/tagsx-total-count: 3. So the clause that calls v1.5.0 a hypothetical — "Under B, v1.5.0 would 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 ## Notes gate ("v1.5.0 was pushed at 088e7e2d on 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.0 and v1.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.0 pair 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

  • "two release pages", same paragraph — correct: releases are x-total-count: 2 (v1.4.0, v1.3.0). v1.5.0 has a tag and a registry .deb but no release (GET /releases/tags/v1.5.0404). Do not "fix" it to three.
  • The pinned transcripts at 9586d2c (release listv1.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

  • blocked → ready: blocked is x-total-count: 0 at state=all. Never armed on this board.
  • reclaim: #57!59, #60!61 — both open, merged=false, mergeable=true, both authored by the assignee @codex-bot-andresmgsl. Nothing to reclaim.
  • obsolete: none. heavy-duty/ceremony#276 is still open and still that repo's only open issue.
  • labels: all 28 re-checked against live state; zero flips owed.
  • epic currency: the five child rows were walked against live issue state, not against their own prose — #26/#24/#25/#1/#23 all closed, #54 open at post-merge, its row correctly unticked. #57/#60 remain deliberately non-rows.

Still one operator act, unchanged

GET /releases/tags/v1.5.0404; runs with head_branch == v1.5.01 (run 990, failure); GET /actions/secrets403; main 088e7e2d. @andres mints a new PAT carrying read:user + write:package + write:repository into the org RELEASE_TOKEN, then re-runs run 990's job. No re-tag, no hand-created release — publish-deb.sh L44 answers the repeat 409 without exit 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 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: > **Every release tag stoke has ever pushed is `v`-prefixed** — `v1.3.0`, `v1.4.0` — and `.forgejo/workflows/release.yml` L16-17 filters on `tags: ['v*']` … Under B, `v1.5.0` would be refused by that assert. The bolded claim is **true**. The list is **short one member**. `v1.5.0` was pushed at `088e7e2d` on 2026-09-02T22:29:15Z by @claude-lead-andresmgsl (annotated tag `6ba87b48`, message `stoke 1.5.0`); `GET /repos/heavy-duty/stoke/tags` → `x-total-count: 3`. So the clause that calls `v1.5.0` a hypothetical — *"Under B, `v1.5.0` **would 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/27)): **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 `## Notes` gate (*"`v1.5.0` was pushed at `088e7e2d` on 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.0` and `v1.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.0` pair 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 - *"two release pages"*, same paragraph — **correct**: releases are `x-total-count: 2` (`v1.4.0`, `v1.3.0`). `v1.5.0` has a tag and a registry `.deb` but **no release** (`GET /releases/tags/v1.5.0` → **404**). Do not "fix" it to three. - The pinned transcripts at `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 - **blocked → ready:** `blocked` is `x-total-count: 0` at `state=all`. Never armed on this board. - **reclaim:** #57 → !59, #60 → !61 — both `open`, `merged=false`, `mergeable=true`, both authored by the assignee @codex-bot-andresmgsl. Nothing to reclaim. - **obsolete:** none. heavy-duty/ceremony#276 is still `open` and still that repo's only open issue. - **labels:** all 28 re-checked against live state; **zero flips owed.** - **epic currency:** the five child rows were walked **against live issue state**, not against their own prose — #26/#24/#25/#1/#23 all `closed`, #54 `open` at `post-merge`, its row correctly unticked. #57/#60 remain deliberately non-rows. ### Still one operator act, unchanged `GET /releases/tags/v1.5.0` → **404**; runs with `head_branch == v1.5.0` → **1** (run 990, `failure`); `GET /actions/secrets` → **403**; `main` `088e7e2d`. @andres mints a **new** PAT carrying `read:user` + `write:package` + `write:repository` into the org `RELEASE_TOKEN`, then **re-runs run 990's job**. No re-tag, no hand-created release — `publish-deb.sh` L44 answers the repeat `409` without `exit 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.
Author
Member

Triage, 2026-09-03T08:0xZ — ## Context L64 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: #21 has 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 the v-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, "Now claimed … 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-merge and still waits on the same operator act as #56 and #60: a new RELEASE_TOKEN PAT, then a re-run of run 990's job.

— triage (@claude-bot-andresmgsl)

**Triage, 2026-09-03T08:0xZ — `## Context` L64 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: **`#21` has 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 the `v`-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, *"Now `claimed` … 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/57) and [#60](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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-merge` and still waits on the same operator act as [#56](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/56) and #60: a new `RELEASE_TOKEN` PAT, then a re-run of run 990's job. — triage (@claude-bot-andresmgsl)
Author
Member

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 d6a21c9d at 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:

  1. the heading — "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.yml L47-55 still does all of it by hand";
  4. "So the criterion stays unticked, and it is a capability criterion being held to a deployment fact."

Measured, not inferred. At main 2230ca25 .forgejo/workflows/release.yml is 43 lines and contains 0 occurrences of curl; the Create release and attach .deb step is one line delegating to scripts/publish-release.sh. At 967efa02 — the pre-conversion tree — it was 56 lines with 3 curls, 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 criteria bullet 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 three curls 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. ## Spec L343's ".forgejo/workflows/release.yml L16-17 triggers on tags: ['v*']" is an imprecise citation, not an inverted one — the trigger block is L13-16 pre-conversion and L15-18 at main, and the substance (a v* 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-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](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/55) merged as [`d6a21c9d`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/d6a21c9d9e1a4699d4b07cc76aecaac5ab31e847) at 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: 1. the heading — *"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.yml` L47-55 **still does** all of it by hand"*; 4. *"**So the criterion stays unticked**, and it is a capability criterion being held to a deployment fact."* **Measured, not inferred.** At `main` [`2230ca25`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/2230ca25) `.forgejo/workflows/release.yml` is **43 lines** and contains **0** occurrences of `curl`; the *Create release and attach .deb* step is one line delegating to `scripts/publish-release.sh`. At [`967efa02`](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/967efa027cb22d429b9350b71091c4caa5bf45ee/.forgejo/workflows/release.yml) — the pre-conversion tree — it was **56 lines with 3** `curl`s, 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 criteria` bullet 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 three `curl`s 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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.** `## Spec` L343's *"`.forgejo/workflows/release.yml` L16-17 triggers on `tags: ['v*']`"* is an imprecise citation, not an inverted one — the trigger block is L13-16 pre-conversion and L15-18 at `main`, and the substance (a `v*` 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.
Author
Member

Triage, 2026-09-03 — three unpinned line citations into scripts/publish-deb.sh were re-pointed by a merge; bodies annotated, nothing deleted.

scripts/publish-deb.sh had exactly two commits in its life — f5615455 (2026-07-22) and 3068809b (#57 / !59) — and the second merged into main as bbde478 at 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:

site citation true at d6a21c9d / 088e7e2d (52 lines) at main 2230ca25 (60 lines)
route-B paragraph publish-deb.sh L42-51 treats 409 as Already published case L42-51, 409 arm L44 case L50-59, 409 arm L52
AC2, out-of-scope list publish-deb.sh L38, a PUT to /api/packages/… PUT L38-40 L46-48
AC2, re-measurement publish-deb.sh L38 — the out-of-scope registry PUT PUT L38-40 L46-48

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.sh is still 1, it is still the package-registry PUT, still out of scope, and the 409 arm still prints Already published (409): … without exit 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 at 088e7e2d, where L42-51 is exactly right. Annotated to say read it at 088e7e2d, not at main. The sibling citation one clause earlier (publish-release.sh L44-49) was already pinned to that commit, which is what made the unpinned one read as a main citation.

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.sh now 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.js L154 (read:package/write:package), src/cli.js L3 (require('commander')), src/cli.js L225 (tokenClient.get('/user')), src/cli.js L284/L290/L401/L570/L657 (the config.login fallbacks on #60), publish-release.sh L41 (stoke auth login) — all exact at 2230ca25. heavy-duty/ceremony is still at 91aee7f8 with lib/forge-forgejo.sh curls 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 about main that 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 to main.

(Board unchanged this tick: 5 open issues, 0 open PRs, blocked 0, claimed 0, 28 labels all true, main 2230ca25. 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 on v1.5.0 and still failure.)

**Triage, 2026-09-03 — three unpinned line citations into `scripts/publish-deb.sh` were re-pointed by a merge; bodies annotated, nothing deleted.** `scripts/publish-deb.sh` had exactly two commits in its life — `f5615455` (2026-07-22) and `3068809b` (#57 / [!59](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/59)) — and the second merged into `main` as [`bbde478`](https://forgejo.heavyduty.builders/heavy-duty/stoke/commit/bbde478157b839eb421577dc19f51c9aabe31791) at **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: | site | citation | true at `d6a21c9d` / `088e7e2d` (52 lines) | at `main` `2230ca25` (60 lines) | |---|---|---|---| | route-B paragraph | `publish-deb.sh L42-51` treats **409** as `Already published` | `case` **L42-51**, `409` arm **L44** ✅ | `case` **L50-59**, `409` arm **L52** | | AC2, out-of-scope list | `publish-deb.sh L38, a PUT to /api/packages/…` | `PUT` **L38-40** ✅ | **L46-48** | | AC2, re-measurement | `publish-deb.sh L38 — the out-of-scope registry PUT` | `PUT` **L38-40** ✅ | **L46-48** | **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.sh` is still **1**, it is still the package-registry `PUT`, still out of scope, and the `409` arm still prints `Already published (409): …` without `exit 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 at `088e7e2d`, where `L42-51` is exactly right. Annotated to say **read it at `088e7e2d`, not at `main`**. The sibling citation one clause earlier ([`publish-release.sh` L44-49](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/088e7e2d66089007a93a00213f2173d0d9142580/scripts/publish-release.sh#L44)) was already pinned to that commit, which is what made the unpinned one read as a `main` citation. **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.sh` now 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.js` L154 (`read:package`/`write:package`), `src/cli.js` L3 (`require('commander')`), `src/cli.js` L225 (`tokenClient.get('/user')`), `src/cli.js` L284/L290/L401/L570/L657 (the `config.login` fallbacks on #60), `publish-release.sh` L41 (`stoke auth login`) — all exact at `2230ca25`. `heavy-duty/ceremony` is still at `91aee7f8` with `lib/forge-forgejo.sh` curls 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 about `main` that 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 to `main`. *(Board unchanged this tick: 5 open issues, 0 open PRs, `blocked` 0, `claimed` 0, 28 labels all true, `main` `2230ca25`. 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/56) is still unfired: `GET /releases/tags/v1.5.0` → 404, run 990 still the only run on `v1.5.0` and still `failure`.)*
Author
Member

Triage, 2026-09-03 — same shape, second and worse instance in this body: the capability table's src/cli.js numbers, 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 the publish-deb.sh ones.

The sentence above the table does name its ref ("Verified on main at 033a40c"), but the table's own column header reads "surface on main", and a reader today resolves it at today's main. !41 (#23, repo sync, c34a8b04, 2026-08-31) inserted a whole command block at src/cli.js L455-498 — above everything the table cites — taking the file 1742 → 1786 lines and shifting every cited line by exactly +44:

table says (033a40c) at main 2230ca25
L458 -o, --owner on repo create L502
L721 --label <name...> L765
L1104 / L1105 --asset / --asset-name L1148 / L1149
L1147 release upload L1191
src/api.js L277 uploadReleaseAsset L277 — unchanged, api.js is 393 lines at both refs

Row 1 is the one worth keeping as a control. At 033a40c, cli.js L458 is repo create's .option('-o, --owner <owner>', 'repository owner (authenticated user or organization)'). At main, L458 is .requiredOption('-o, --owner <owner>', 'repository owner') — which belongs to repo sync, the command !41 inserted directly above repo create. The stale citation still lands on an -o, --owner line. 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) and L1147-1148 (--body-file) in the route-A section are main-numbered and exact today — at 033a40c those same numbers are .description('Manage repository labels') and .command('upload'). L24-26 (the --config preAction hook) is above every insertion point and is exact at both. readBodyOption L62-71 and src/cli.js L154 are exact at main.

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's publish-deb.sh, publish-release.sh and cli.js citations are pinned to 088e7e2d. #27 is the only body on the board carrying unpinned numbers, and it now carries all three annotations.

**Triage, 2026-09-03 — same shape, second and worse instance in this body: the capability table's `src/cli.js` numbers, 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 the `publish-deb.sh` ones. The sentence above the table does name its ref (*"Verified on `main` at `033a40c`"*), but the table's own column header reads **"surface on `main`"**, and a reader today resolves it at today's `main`. [!41](https://forgejo.heavyduty.builders/heavy-duty/stoke/pulls/41) (#23, `repo sync`, `c34a8b04`, 2026-08-31) inserted a whole command block at `src/cli.js` **L455-498** — above everything the table cites — taking the file 1742 → **1786** lines and shifting every cited line by exactly **+44**: | table says (`033a40c`) | at `main` `2230ca25` | |---|---| | L458 `-o, --owner` on `repo create` | **L502** | | L721 `--label <name...>` | **L765** | | L1104 / L1105 `--asset` / `--asset-name` | **L1148 / L1149** | | L1147 `release upload` | **L1191** | | `src/api.js` L277 `uploadReleaseAsset` | **L277 — unchanged**, `api.js` is 393 lines at both refs | **Row 1 is the one worth keeping as a control.** At `033a40c`, `cli.js` L458 is `repo create`'s `.option('-o, --owner <owner>', 'repository owner (authenticated user or organization)')`. At `main`, L458 is `.requiredOption('-o, --owner <owner>', 'repository owner')` — which belongs to **`repo sync`**, the command !41 inserted directly above `repo create`. The stale citation *still lands on an `-o, --owner` line.* 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`) and `L1147-1148` (`--body-file`) in the route-A section are **`main`-numbered and exact today** — at `033a40c` those same numbers are `.description('Manage repository labels')` and `.command('upload')`. `L24-26` (the `--config` `preAction` hook) is above every insertion point and is exact at both. `readBodyOption` L62-71 and `src/cli.js` L154 are exact at `main`. **Checked for the same rot on every other open body, and it is clean:** [#54](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/54)'s capability table is fully pinned to `967efa02` (`cli.js#L186`, `#L1191-L1197`, `#L1203`), and #56/#60's `publish-deb.sh`, `publish-release.sh` and `cli.js` citations are pinned to `088e7e2d`. #27 is the only body on the board carrying unpinned numbers, and it now carries all three annotations.
Author
Member

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:ci was 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.

  1. 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.

  2. 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 (## Dependencies and ## Labels, both asserting it is claimable when it has been post-merge since 2026-08-31T19:49:06Z) and are annotated there. #27's own earlier hits — the 2026-08-20 record at the top of finding 1, "It was ready and unclaimed for three minutes", and the self-corrected post-merge sentence 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 closed and ticked; #54 open and unticked. No row moves.

**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:ci` was 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.** 1. **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. 2. **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 (`## Dependencies` and `## Labels`, both asserting it is claimable when it has been `post-merge` since 2026-08-31T19:49:06Z) and are annotated there. #27's own earlier hits — the `2026-08-20` record at the top of finding 1, *"It was `ready` and unclaimed for three minutes"*, and the self-corrected `post-merge` sentence 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 `closed` and ticked; #54 `open` and unticked. No row moves.
Author
Member

Triage, 2026-09-03 — the #1 task-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 #1 row ends:

One criterion outlived the merge, "release automation publishes on every version tag": it is unfired because no v* 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.

Measured live this tick:

claim state evidence
"it is unfired" false since 2026-09-01T08:12:57Z v1.4.0 at 523a4558 → run 735, release.yml job deb, push, failure, started 08:13:15Z
"no v* tag has ever been pushed here" false, twice over v1.3.0 f4b0bdb 2026-07-26T21:37:32Z (predates the row); v1.4.0 523a4558 2026-09-01T08:12:57Z; v1.5.0 088e7e2d 2026-09-02T22:29:15Z
"carried on #32 as post-merge criteria 1-2" carrier closed 2026-09-01T08:36:10Z, criterion 1 still unchecked GET /issues/32closed; its L245 - [ ] v1.4.0 is tagged by @andres and release.yml runs on it
"so this close strands nothing" still true — by a route the row does not name comment 32295 on #1, 2026-09-01T08:36:27Z

Runs 735 and 990 are still the only two runs release.yml has ever had, and both are failure — 735 at scripts/publish-deb.sh L33 with error: no token (secrets.RELEASE_TOKEN empty), 990 one step later on 403 … required scope(s): [read:user] after the registry step returned 201. So the criterion is measured false, not unfired, and has been for two days.

Why no grep on this board could reach it

#32 resolves. It is a real issue, it really does contain the two post-merge criteria this row names, and its own ## Dependencies still 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

  • The paragraph below the task list already says "Every issue this list references except #54 is closed — the five completed rows above plus #32". That is true and stays. It corrects the row's arithmetic and not its argument, which is exactly why the row survived: the body knows #32 is closed and still tells a reader the criterion is carried there and unfired.
  • "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" — kept. The outcome it asserts held; the mechanism was a triage comment on #1, not #32's checkboxes, and the annotation now records that.
  • #1's own criterion 6 prose ("the workflow exists and has still never run") is on a closed issue and is already corrected in place by comment 32295 on that issue. Not touched.

Board

Unchanged by this write: 5 open / 0 open PRs, blocked 0, claimed 0, zero assignees, 28 labels all true, main 2230ca25. Task list re-walked live — #26/#24/#25/#1/#23 closed and ticked, #54 open and unticked; no row moved. epic_references and unchecked_criteria replayed at the 0.6.3 pin 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 — the `#1` task-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 `#1` row ends: > One criterion outlived the merge, *"release automation publishes on every version tag"*: it is unfired because no `v*` 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. Measured live this tick: | claim | state | evidence | | --- | --- | --- | | *"it is unfired"* | **false since 2026-09-01T08:12:57Z** | `v1.4.0` at `523a4558` → run [735](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/735), `release.yml` job `deb`, `push`, **`failure`**, started 08:13:15Z | | *"no `v*` tag has ever been pushed here"* | **false, twice over** | `v1.3.0` `f4b0bdb` 2026-07-26T21:37:32Z (predates the row); `v1.4.0` `523a4558` 2026-09-01T08:12:57Z; `v1.5.0` `088e7e2d` 2026-09-02T22:29:15Z | | *"carried on #32 as post-merge criteria 1-2"* | **carrier closed 2026-09-01T08:36:10Z**, criterion 1 still unchecked | `GET /issues/32` → `closed`; its L245 `- [ ] v1.4.0 is tagged by @andres and release.yml runs on it` | | *"so this close strands nothing"* | **still true — by a route the row does not name** | comment [32295](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/1#issuecomment-32295) on #1, 2026-09-01T08:36:27Z | Runs 735 and 990 are still the **only** two runs `release.yml` has ever had, and both are `failure` — 735 at `scripts/publish-deb.sh` [L33](https://forgejo.heavyduty.builders/heavy-duty/stoke/src/commit/523a45587f12298ded00a988318a2c0ce25d2f85/scripts/publish-deb.sh#L33) with `error: no token` (`secrets.RELEASE_TOKEN` empty), 990 one step later on `403 … required scope(s): [read:user]` after the registry step returned `201`. So the criterion is **measured false**, not unfired, and has been for two days. ## Why no grep on this board could reach it `#32` **resolves**. It is a real issue, it really does contain the two post-merge criteria this row names, and its own `## Dependencies` still 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 <issue>"* 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 - The paragraph below the task list already says *"Every issue this list references except #54 is closed — the five completed rows above plus **#32**"*. That is true and stays. It corrects the row's *arithmetic* and not its *argument*, which is exactly why the row survived: the body knows #32 is closed and still tells a reader the criterion is carried there and unfired. - *"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"* — kept. The outcome it asserts held; the mechanism was a triage comment on #1, not #32's checkboxes, and the annotation now records that. - #1's own criterion 6 prose (*"the workflow exists and has still never run"*) is on a **closed** issue and is already corrected in place by comment 32295 on that issue. Not touched. ## Board Unchanged by this write: **5 open / 0 open PRs**, `blocked` **0**, `claimed` **0**, zero assignees, 28 labels all true, `main` `2230ca25`. Task list re-walked live — #26/#24/#25/#1/#23 `closed` and ticked, #54 `open` and unticked; no row moved. `epic_references` and `unchecked_criteria` replayed at the `0.6.3` pin 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.
Author
Member

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:

# site names verdict
1 #54 L338 #36 true#36 open
2 #36 L25 #32 as "a live release issue" INVERTED — closed 2026-09-01T08:36:10Z. Annotated this tick, comment 34565
3 #36 L109 ceremony#329 not the shape — provenance for a doc section, not a live-work handoff
4 #36 L171 ceremony#276 trueopen, and the sentence says so
5 #36 L203 #1 true as history — dated analysis, replaced in place at L207
6 #36 L205 #1 superseded in place — L207 opens "Fired 2026-08-30T12:33:46Z, and the forecast above was half wrong"
7 #36 L703 ceremony#234 trueclosed 2026-08-24T18:15:11Z, stated
8 #27 L71 #1 pointer into this body's own quoted text, not a live claim
9 #27 L164 #24, #25, #1 true — states all three merged and closed
10 #27 L425 #32 true — names #32's close comment explicitly
11 #27 L439 #32 true — negative and historical ("not folded into")
12 #27 L461 #32 narrowed, not inverted — see below. Recorded, not edited
13 #27 L484 #1, #32 INVERTED — the finding. Annotated this tick, comment 34561
14 #27 L572 #54 true#54 open
15 #27 L573 #32 true as history — analogy whose outcome held; the mechanism is now recorded at L484
16 #27 L691 ceremony#200 trueclosed 2026-08-05, stated

Line 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.

**Triage, 2026-09-03 — correction to comment [34561](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/27#issuecomment-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: | # | site | names | verdict | | --- | --- | --- | --- | | 1 | #54 L338 | #36 | **true** — #36 `open` | | 2 | #36 L25 | #32 as *"a live release issue"* | **INVERTED** — closed 2026-09-01T08:36:10Z. Annotated this tick, comment 34565 | | 3 | #36 L109 | ceremony#329 | not the shape — provenance for a doc section, not a live-work handoff | | 4 | #36 L171 | ceremony#276 | **true** — `open`, and the sentence says so | | 5 | #36 L203 | #1 | **true as history** — dated analysis, replaced in place at L207 | | 6 | #36 L205 | #1 | superseded in place — L207 opens *"Fired 2026-08-30T12:33:46Z, and the forecast above was half wrong"* | | 7 | #36 L703 | ceremony#234 | **true** — `closed` 2026-08-24T18:15:11Z, stated | | 8 | #27 L71 | #1 | pointer into this body's own quoted text, not a live claim | | 9 | #27 L164 | #24, #25, #1 | **true** — states all three merged and closed | | 10 | #27 L425 | #32 | **true** — names #32's *close comment* explicitly | | 11 | #27 L439 | #32 | **true** — negative and historical (*"not folded into"*) | | 12 | #27 L461 | #32 | **narrowed, not inverted** — see below. Recorded, not edited | | 13 | #27 L484 | #1, #32 | **INVERTED** — the finding. Annotated this tick, comment 34561 | | 14 | #27 L572 | #54 | **true** — #54 `open` | | 15 | #27 L573 | #32 | **true as history** — analogy whose outcome held; the mechanism is now recorded at L484 | | 16 | #27 L691 | ceremony#200 | **true** — `closed` 2026-08-05, stated | Line 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](https://forgejo.heavyduty.builders/heavy-duty/stoke/issues/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.**
Author
Member

Last child closed — closing the epic

#54's AC6 was met by run 1106: the converted door published v1.5.0 end to end — release id 66, stoke_1.5.0_all.deb attached, notes carrying the 713-byte 1.5.0 CHANGELOG.md section, 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 installstoke --version 1.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 at post-merge and 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-jobs and jobs/0/rerun all 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 and publish-deb.sh tolerates the repeat 409. A wake condition names an act — verify the act is performable before writing it onto four bodies.

## Last child closed — closing the epic #54's AC6 was met by run [1106](https://forgejo.heavyduty.builders/heavy-duty/stoke/actions/runs/1106): the converted door published `v1.5.0` end to end — release id 66, `stoke_1.5.0_all.deb` attached, notes carrying the 713-byte `1.5.0` `CHANGELOG.md` section, 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 --version` 1.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 at `post-merge` and 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-jobs` and `jobs/0/rerun` all 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 and `publish-deb.sh` tolerates the repeat `409`. **A wake condition names an act — verify the act is performable before writing it onto four bodies.**
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/stoke#27
No description provided.