bug(decide): rt decide reads tags and prepare commits as v-prefixed, so a release-{version} adopter's merged prepare is never detected as a cut #1533
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1533
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
rt decideloadstag_format(cmd/rt/decide.go:160) but uses it only to render the next tag (internal/decide/decide.go:1351-1358). Every place it reads a tag or a prepare commit assumes avprefix.PR #1531's body names the
--match 'v*'describe atinternal/decide/git.go:228. On the manifest path that site is unreachable. Three other sites are reachable, and one of them is the cut detector.Measured
rtwas built from origin/main8086700and run with--dry-runagainst a forge URL that does not answer. The scratch adopter hadtag_format: 'release-{version}', tagrelease-0.1.0on the first commit, and oneaddedfragment. The control is the same repo withtag_format: 'v{version}'and tagv0.1.0.release-{version}v{version}virtual-anchor bootstrap (#368) ... anchor v0.0.0;next_version=0.1.0,next_tag=release-0.1.0, a tag that already existsbootstrapped: last_released_tag=v0.1.0;next_tag=v0.2.0last_released_tag: release-0.1.0next_tag=release-0.2.0(correct)chore(release): prepare release-0.2.0landsmode=blocked,reason=pending_cut,pending_prep_sha=unknownLayer 1 (subject-regex) MATCH: version=0.2.0release-0.2.0presentpending_tag_exists=false, and the message says to rungit tag --list v0.2.0The control's Layer 2 could not grade against the unreachable forge and fell through to update. That is expected offline. The point is that Layer 1 matched for
vand did not match forrelease-.The four sites, read at origin/main:
internal/decide/git.go:50definesstableTagRE = ^v?[0-9]+\.[0-9]+\.[0-9]+$, andlatestStableTag(:85-97) uses it. Norelease-*tag matches.loadOrBootstrap(decide.go:297-306) takes the #368 virtual v0.0.0 anchor and walks from the root commit, as if the repo had never released.:310also strips a literalvto get the version.internal/decide/prepsubject.go:26-28admits onlychore(release): [prepare ]v?X.Y.Z.rt prepwrites the commitchore(release): prepare <newTag>(cmd/rt/prep.go:736) and the PR titlechore(release): <newTag>(:789). Forrelease-0.2.0, neither the commit nor a squash title matches.decide.go:569-588) never fires. The #417 orphan walk (decide.go:459-460) cannot find the prepare either, and a merged prepare reads as a cancelled cut.IsPrepMergeSubjecthas two more callers:cmd/rt/check_self_bootstrap.go:124andcmd/rt/prep_order_check.go:131.pendingCutEvidence(decide.go:1541-1546) looks uptag := "v" + versioninstead ofd.renderTag(version), for both the git tag and the forge release. For a non-vadopter, both reads answer "absent" whatever exists.rt recover-pending-cuttakes those two fields as given (cmd/rt/recover_pending_cut.go:204-218).tag_format(:237), so the control finds its release and passes.vadopter, but only by accident. Site 2 leaves the prepare SHA unknown, and the restore step refuses on that (recover_pending_cut.go:351-353, "pending prepare SHA is unavailable").release-0.2.0but its post-cut bookkeeping never landed. It would gradetag_exists=falseandrelease_published=absent, with the control present. That is the combination in which recovery (B) folds a published section back under[Unreleased], which is exactly what #885 and #1457 exist to prevent.--match 'v*'atgit.go:228.resolveCCRange(decide.go:1277-1288) calls it only when the anchor tag is empty.Does the manifest path hide it? It hides site 4 only.
autoBumpwalks from the anchor's tag (decide.go:1240). On the manifest path that islast_released_tag.docs/architecture/contracts/manifest.schema.jsonmakes that field required, andrt post-cutwrites the rendered tag into it (cmd/rt/post_cut.go:585).git.go:228is reached only by a manifest that omitslast_released_tag.loadManifest(decide.go:381-386) requires only the SHA and the version, so such a manifest is schema-invalid but accepted.PR #1531 (head
6979efc) addsTagGlobsFor(internal/prep/git.go:108on that head), which derivesdescribeglobs fromtag_format. It uses them only for the opt-in commit-note walk.internal/decideis untouched.Census, 2026-10-03: storey, ember, tmux-tell, cellblock and purser all use
tag_format: 'v{version}'. Nobody on this instance is affected today, which is why this is priority/low. It becomes reachable as soon asrt prep's doc-refs refusal (the sibling issue) is lifted.Why it matters
Lifting the prep refusal is not enough to cut. Once that refusal is gone, a non-
vadopter's first merged prepare still lands inmode=blocked.The adopter is stuck, with no working route offered. With a reachable forge, the release lookup for
v0.2.0returns 404, sorelease_published=absent. The blocked message's default arm (internal/decide/pending.go:144) then offers two routes:rt recover-pending-cut, which refuses because it has no prepare SHA.(Read, not run.)
Site 3 is dangerous once site 2 is corrected. It hands the recovery verb evidence that is wrong in the destructive direction, and the verb's control cannot catch it, because the control renders the tag correctly. Sites 2 and 3 have to change together.
A manifest-free adoption repeats an existing version. With
release-*tags already present, it proposes a version that exists (site 1) and walks the whole history for the bump. That is the state #1483 tracks, here on a repo that has in fact released.Decision tree
vformats in decide.tag_format:pendingCutEvidenceusesrenderTag;IsPrepMergeSubject's other two callers move with it;^v?;TagGlobsFor, if #1531 lands with it).v{version}formats at config load. This is the prep sibling's option (c), shared.rt prepandrt manifest-check, which load the config strictly.rt decideloads it best-effort and falls back to defaults on a load failure (cmd/rt/decide.go:144-148), so a refusal at load would not stop decide on its own.Acceptance criteria
tag_format: 'release-{version}'that is red on origin/main8086700and green after (mutation-verified, AGENTS.md section 8 Axis B).release-X.Y.Ztag and a published release, in the style ofinternal/decide/pending_evidence_test.go.v.IsPrepMergeSubject's callers (check_self_bootstrap.go:124,prep_order_check.go:131) agree with decide's new matching, or this issue records why they keepv.git tag --listhint names the rendered tag (internal/decide/pending.go:125).Out of scope
rt prep's doc-refs refusal. That is the sibling issue.v*describes outside decide. The same change should convert them, or each should get its own issue:rt prep's bump and commit walk:internal/prep/git.go:70,80andinternal/prep/gitsource.go:71.rt manifest-check's tag-vs-manifest check (cmd/rt/manifest_check.go:185-188), which passes vacuously for a non-vadopter. On the scratch repo it printedOK: no prior v* tag (first release; nothing to compare against)whilerelease-0.1.0existed.cmd/rt/prune_rc_tags.go:82("v" + version + "-rc."). It is toolkit-self rc bookkeeping, dormant per AGENTS.md section 2.4.Anchor
Found while reviewing PR #1531 / issue #1529 on 2026-10-03. #1531's body lists "
rt decideandv*" under "Not in this PR (each needs its own issue)". Probing it showed thatgit.go:228is latent behind the manifest, and that the cut detector and the pending-cut evidence are the live defects. Related: #885, #1457, #1483, #417, #368.The sibling issue this body refers to is #1532 (a stable
rt preprefuses every tag that is notvX.Y.Z). Both were found while reviewing PR #1531.