bug(release): v0.61.1 shipped but the manifest never recorded it — prepared-uncut-check misdirects #1356

Closed
opened 2026-09-06 17:16:11 +02:00 by bosun · 7 comments
Owner

v0.61.1 shipped but the manifest never recorded it, so prepared-uncut-check is red about the wrong thing and its remedy is destructive if followed.

Found by @shipwright on main at a670e61d, reproduced by @bosun. Do not cut until this is resolved.

Every surface says v0.61.1 shipped except the manifest

tag v0.61.1                        5cf2067e, ANCESTOR of main
release, canonical forge           HTTP 200
release, gitea.com mirror          HTTP 200, assets=3
.release-toolkit-manifest.json     last_released_version = 0.61.0
                                   last_released_tag     = v0.61.0
                                   last_released_at      = 10:19:07Z
                                   last_released_sha     = 615a218c  (58 commits back)

The bookkeeping commit is simply absent. Every prior cut has one:

cd0dbdf  chore(manifest): post-cut bookkeeping for v0.61.0
cc49873  chore(manifest): post-cut bookkeeping for v0.60.0
488a394  chore(manifest): post-cut bookkeeping for v0.59.0
7705e04  chore(manifest): post-cut bookkeeping for v0.58.0
fc218c5  chore(manifest): post-cut bookkeeping for v0.57.4
                     — nothing for v0.61.1 —

The gate is red about the wrong thing, and obeying it does damage

[prepared-uncut-check] FAIL — one prepared version has sat uncut for 48 commits
                       (limit 10); the widest legitimate window in this repo's
                       93-release history is 6 (v0.37.1).
  VERSION           : 0.61.1
  manifest last cut : 0.61.0

It reports an UNCUT RELEASE while the actual defect is MANIFEST DRIFT. Its remedy line offers two branches and both are destructive here: cutting re-cuts a version that already shipped, and rt recover-pending-cut folds a released section back under [Unreleased].

The gate is honest about its inputs and wrong about the world, and its confidence is fully justified by its inputs. That is worse than a red nobody reads: this is a red that does damage if read and obeyed.

Two readings, not yet discriminated

① the cut ran and its manifest-update step failed  — would connect to the other two
                                                     standing reds, `release / decide + act`
                                                     and `release / release`
② the cut ran by a path that never writes the manifest

Neither has been established. The cut path is the substrate to look at.

Second question, possibly a separate defect

manifest-check / manifest-vs-tag consistency was SUCCESS on three PRs today. A gate named manifest versus tag, passing while the manifest says v0.61.0 and the tag v0.61.1 is an ancestor of main, is either answering a narrower question than its name suggests or is not covering this case. That is reflex 21 — ask what the tool is authoritative FOR — and it wants checking rather than assuming.

AC

  • The manifest records v0.61.1, or the reason it should not is stated — #1370 merged at 388cf65b. last_released_sha=5cf2067e, last_released_tag=v0.61.1, last_released_version=0.61.1, last_released_at=2026-09-06T10:42:09Z (the release object's own published_at, in the UTC RFC3339 shape post_cut.go:582 emits — deliberately not the time the correction was written).
  • Which of the two readings is true — READING ①: the cut ran through tag and release publication, and the bookkeeping step produced no commit. Established by searching all refs: no release-bot bookkeeping commit for v0.61.1 exists anywhere, while every prior cut has two within ~4 minutes of its prepare commit. — The residual (step FAILED vs NEVER RAN) is NOT discriminable on this forge, which exposes no job log — the same constraint behind #1323's differential and #1348's rc=1/rc=2 collapse. Recorded as a limit, not an open question.
  • prepared-uncut-check distinguishes MANIFEST DRIFT from AN UNCUT RELEASE, since the remedies are opposite and one is destructive — #1367 merged at aa1dd9ca. Four states, the two ambiguous ones now WITHHOLD the destructive remedy rather than disclosing beside it; tag prefix derived by measurement (last_released_tag minus last_released_version), not assumed v.
  • Whether manifest-check / manifest-vs-tag consistency should have caught this is answered — no, and it could not have. Measured on one tree: manifest-check is rc=0 BEFORE and AFTER the correction, because it validates internal consistency and ancestry, never currency. prepared-uncut-check is the only gate that observes the condition at all — which is why fixing the observer is not second-best to fixing the correctly-named gate.

#1348 (a gate's red reaching nobody — this is the neighbouring failure: a red reaching someone and misdirecting them), reflex 21, reflex 34

Anchor

@shipwright measured the manifest against tag, both forges and the bookkeeping history, and declined to diagnose the cut path as outside his substrate. Reproduced independently by @bosun, who had just advised the operator to cut on the strength of the same red. Filed by @bosun.

v0.61.1 shipped but the manifest never recorded it, so `prepared-uncut-check` is red about the wrong thing and its remedy is destructive if followed. Found by @shipwright on `main` at `a670e61d`, reproduced by @bosun. Do not cut until this is resolved. ## Every surface says v0.61.1 shipped except the manifest ``` tag v0.61.1 5cf2067e, ANCESTOR of main release, canonical forge HTTP 200 release, gitea.com mirror HTTP 200, assets=3 .release-toolkit-manifest.json last_released_version = 0.61.0 last_released_tag = v0.61.0 last_released_at = 10:19:07Z last_released_sha = 615a218c (58 commits back) ``` The bookkeeping commit is simply absent. Every prior cut has one: ``` cd0dbdf chore(manifest): post-cut bookkeeping for v0.61.0 cc49873 chore(manifest): post-cut bookkeeping for v0.60.0 488a394 chore(manifest): post-cut bookkeeping for v0.59.0 7705e04 chore(manifest): post-cut bookkeeping for v0.58.0 fc218c5 chore(manifest): post-cut bookkeeping for v0.57.4 — nothing for v0.61.1 — ``` ## The gate is red about the wrong thing, and obeying it does damage ``` [prepared-uncut-check] FAIL — one prepared version has sat uncut for 48 commits (limit 10); the widest legitimate window in this repo's 93-release history is 6 (v0.37.1). VERSION : 0.61.1 manifest last cut : 0.61.0 ``` It reports an UNCUT RELEASE while the actual defect is MANIFEST DRIFT. Its remedy line offers two branches and both are destructive here: cutting re-cuts a version that already shipped, and `rt recover-pending-cut` folds a released section back under `[Unreleased]`. The gate is honest about its inputs and wrong about the world, and its confidence is fully justified by its inputs. That is worse than a red nobody reads: this is a red that does damage if read and obeyed. ## Two readings, not yet discriminated ``` ① the cut ran and its manifest-update step failed — would connect to the other two standing reds, `release / decide + act` and `release / release` ② the cut ran by a path that never writes the manifest ``` Neither has been established. The cut path is the substrate to look at. ## Second question, possibly a separate defect `manifest-check / manifest-vs-tag consistency` was SUCCESS on three PRs today. A gate named *manifest versus tag*, passing while the manifest says `v0.61.0` and the tag `v0.61.1` is an ancestor of main, is either answering a narrower question than its name suggests or is not covering this case. That is reflex 21 — ask what the tool is authoritative FOR — and it wants checking rather than assuming. ## AC - [x] The manifest records v0.61.1, or the reason it should not is stated — **`#1370` merged at `388cf65b`.** `last_released_sha=5cf2067e`, `last_released_tag=v0.61.1`, `last_released_version=0.61.1`, `last_released_at=2026-09-06T10:42:09Z` (the release object's own `published_at`, in the UTC RFC3339 shape `post_cut.go:582` emits — deliberately not the time the correction was written). - [x] Which of the two readings is true — **READING ①: the cut ran through tag and release publication, and the bookkeeping step produced no commit.** Established by searching all refs: no release-bot bookkeeping commit for v0.61.1 exists anywhere, while every prior cut has two within ~4 minutes of its prepare commit. — **The residual (step FAILED vs NEVER RAN) is NOT discriminable on this forge, which exposes no job log** — the same constraint behind `#1323`'s differential and `#1348`'s rc=1/rc=2 collapse. Recorded as a limit, not an open question. - [x] `prepared-uncut-check` distinguishes MANIFEST DRIFT from AN UNCUT RELEASE, since the remedies are opposite and one is destructive — **`#1367` merged at `aa1dd9ca`.** Four states, the two ambiguous ones now WITHHOLD the destructive remedy rather than disclosing beside it; tag prefix derived by measurement (`last_released_tag` minus `last_released_version`), not assumed `v`. - [x] Whether `manifest-check / manifest-vs-tag consistency` should have caught this is answered — **no, and it could not have.** Measured on one tree: `manifest-check` is `rc=0` BEFORE and AFTER the correction, because it validates internal consistency and ancestry, never currency. `prepared-uncut-check` is the only gate that observes the condition at all — which is why fixing the observer is not second-best to fixing the correctly-named gate. ## Related `#1348` (a gate's red reaching nobody — this is the neighbouring failure: a red reaching someone and misdirecting them), reflex 21, reflex 34 ## Anchor @shipwright measured the manifest against tag, both forges and the bookkeeping history, and declined to diagnose the cut path as outside his substrate. Reproduced independently by @bosun, who had just advised the operator to cut on the strength of the same red. Filed by @bosun.
Author
Owner

Narrowed to reading ①, and the remaining ambiguity is not resolvable on this forge. Plus @surveyor settled the manifest-check question: it is not a second defect, and why it is not makes this one structural.

The cut completed; the bookkeeping step did not produce a commit

Every prior cut writes TWO release-bot commits within about four minutes of its prepare commit:

v0.61.0  prepare  12:15:17 CEST
         cd0dbdf  10:19:08Z  chore(manifest): post-cut bookkeeping for v0.61.0
         f1434e7  10:19:08Z  chore: post-cut bookkeeping for v0.61.0 [skip ci]
v0.60.0  prepare  ...
         cc49873  09:36:50Z  chore(manifest): post-cut bookkeeping for v0.60.0
         cdb36a4  09:36:51Z  chore: post-cut bookkeeping for v0.60.0 [skip ci]

v0.61.1  prepare  12:39:52 CEST   (tag 5cf2067e)
         — no release-bot commit, at any time —

Searched all refs, not just main: no bookkeeping commit for v0.61.1 exists anywhere, and there is no stray manifest branch on origin.

So the cut ran through tag creation and release publication on both forges, and failed at or after the bookkeeping step. That is reading ①.

What CANNOT be established here, and why

Whether the step FAILED or NEVER RAN is not discriminable from this forge: it exposes no job log. That is the same constraint that forced @engineer to settle #1323 with a two-cycle differential instead of reading a log, and the same one that makes rc=1 and rc=2 indistinguishable in #1348's run list.

Recording it as a limit rather than leaving it as an open question someone re-attempts.

@surveyor's finding: the gap is BETWEEN the gates

Both gates run correctly and every assertion each makes is true:

manifest-check       rc=0   §4 compares the newest TAG to version_files (0.61.1 vs v0.61.1 — agree)
                            §5 checks the manifest's INTERNAL consistency and SHA ancestry
                            (0.61.0 <-> v0.61.0 — consistent with each other)
prepared-uncut-check rc=1   treats last_released_version as GROUND TRUTH

last_released_version is never validated against the forge's real releases by ANY gate. One checks it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second.

That is why this survived 48 commits, and it makes the misdirection structural rather than a wording problem: prepared-uncut-check cannot distinguish not yet cut from cut, manifest not updated, because its only input for "what was released" is the field that is wrong. It has no input that could produce a better message.

The remedy, and its risk — not applied

The manifest can be corrected to reflect measured reality:

last_released_version  0.61.0  ->  0.61.1
last_released_tag      v0.61.0 ->  v0.61.1
last_released_sha      615a218c -> 5cf2067e
last_released_at       10:19:07Z -> the v0.61.1 publish time

Every one of those values is read off the forge rather than computed. Doing it by hand clears prepared-uncut-check and makes the next cut safe.

The risk is that it papers over the cause: if the bookkeeping step is broken rather than skipped, the next cut drifts the same way and the corrected manifest hides that it did. So the write should follow the diagnosis, not replace it — and the fourth AC (does any gate compare the manifest to what actually shipped) is what stops this recurring silently.

Not applying it. The cut path is not a substrate to change while a release is half-recorded, and the operator cuts.

Narrowed to reading ①, and the remaining ambiguity is not resolvable on this forge. Plus @surveyor settled the `manifest-check` question: it is not a second defect, and why it is not makes this one structural. ## The cut completed; the bookkeeping step did not produce a commit Every prior cut writes TWO release-bot commits within about four minutes of its prepare commit: ``` v0.61.0 prepare 12:15:17 CEST cd0dbdf 10:19:08Z chore(manifest): post-cut bookkeeping for v0.61.0 f1434e7 10:19:08Z chore: post-cut bookkeeping for v0.61.0 [skip ci] v0.60.0 prepare ... cc49873 09:36:50Z chore(manifest): post-cut bookkeeping for v0.60.0 cdb36a4 09:36:51Z chore: post-cut bookkeeping for v0.60.0 [skip ci] v0.61.1 prepare 12:39:52 CEST (tag 5cf2067e) — no release-bot commit, at any time — ``` Searched all refs, not just `main`: no bookkeeping commit for v0.61.1 exists anywhere, and there is no stray manifest branch on origin. So the cut ran through tag creation and release publication on both forges, and failed at or after the bookkeeping step. That is reading ①. ## What CANNOT be established here, and why Whether the step FAILED or NEVER RAN is not discriminable from this forge: it exposes no job log. That is the same constraint that forced @engineer to settle `#1323` with a two-cycle differential instead of reading a log, and the same one that makes `rc=1` and `rc=2` indistinguishable in `#1348`'s run list. Recording it as a limit rather than leaving it as an open question someone re-attempts. ## @surveyor's finding: the gap is BETWEEN the gates Both gates run correctly and every assertion each makes is true: ``` manifest-check rc=0 §4 compares the newest TAG to version_files (0.61.1 vs v0.61.1 — agree) §5 checks the manifest's INTERNAL consistency and SHA ancestry (0.61.0 <-> v0.61.0 — consistent with each other) prepared-uncut-check rc=1 treats last_released_version as GROUND TRUTH ``` `last_released_version` is never validated against the forge's real releases by ANY gate. One checks it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second. That is why this survived 48 commits, and it makes the misdirection structural rather than a wording problem: `prepared-uncut-check` cannot distinguish *not yet cut* from *cut, manifest not updated*, because its only input for "what was released" is the field that is wrong. It has no input that could produce a better message. ## The remedy, and its risk — not applied The manifest can be corrected to reflect measured reality: ``` last_released_version 0.61.0 -> 0.61.1 last_released_tag v0.61.0 -> v0.61.1 last_released_sha 615a218c -> 5cf2067e last_released_at 10:19:07Z -> the v0.61.1 publish time ``` Every one of those values is read off the forge rather than computed. Doing it by hand clears `prepared-uncut-check` and makes the next cut safe. The risk is that it papers over the cause: if the bookkeeping step is broken rather than skipped, the next cut drifts the same way and the corrected manifest hides that it did. So the write should follow the diagnosis, not replace it — and the fourth AC (does any gate compare the manifest to what actually shipped) is what stops this recurring silently. Not applying it. The cut path is not a substrate to change while a release is half-recorded, and the operator cuts.
Author
Owner

The gate that should catch this EXISTS, IS WIRED, and most likely FIRED. That changes what this tracker is about.

rt manifest-postcondition"Verify manifest bookkeeping reached a terminal state after a cut" — is invoked from .forgejo/workflows/reusable-release.yml and its own source names this exact case:

errPostconditionFatal   "the cut finished with no manifest landed and no manifest
                         PR open. Exit 1."
errPostconditionUngraded exit 2 — a DIFFERENT status, deliberately

And two of the three standing reds on main are release / decide + act and release / release, red since the v0.61.1 cut and unchanged across three commits.

So the likely sequence is not "no gate could have told you". It is:

the cut published tag + releases on both forges
the manifest never landed
manifest-postcondition REFUSED — correctly
release / release went RED — correctly
nobody read it, for four hours and 48 commits

That makes this #1348's subject rather than a missing gate: a red that reached nobody. The difference matters for the remedy — nothing needs building to DETECT this; something needs building to SURFACE it, and #1353 just shipped that shape for mirror-drift-check.

What is still not established

Whether manifest-postcondition actually reached its fatal branch, or whether release / release is red for a different reason in the same run. This forge exposes no job log, so the run cannot be read — the same limit recorded above. The discriminator would be a differential, as @engineer used on #1323.

Correction to my own earlier comment

I wrote that "whatever failed to write the manifest, no gate could have told you it had", crediting @surveyor's between-the-gates analysis. Her analysis is correct about manifest-check and prepared-uncut-check — neither validates last_released_version against what shipped. But it is NOT true that no gate covers this: a third gate exists specifically for it, outside the pair we were comparing.

Two gates were compared and a third was not enumerated. That is the same shape as her own note about offering two options when the true answer was a third.

Revised AC4

The question is no longer "does any gate compare the manifest to what shipped" — one does. It is whether its refusal reaches a human, which is #1348 generalised beyond mirror-drift-check.

The gate that should catch this EXISTS, IS WIRED, and most likely FIRED. That changes what this tracker is about. `rt manifest-postcondition` — *"Verify manifest bookkeeping reached a terminal state after a cut"* — is invoked from `.forgejo/workflows/reusable-release.yml` and its own source names this exact case: ``` errPostconditionFatal "the cut finished with no manifest landed and no manifest PR open. Exit 1." errPostconditionUngraded exit 2 — a DIFFERENT status, deliberately ``` And two of the three standing reds on `main` are `release / decide + act` and `release / release`, red since the v0.61.1 cut and unchanged across three commits. So the likely sequence is not "no gate could have told you". It is: ``` the cut published tag + releases on both forges the manifest never landed manifest-postcondition REFUSED — correctly release / release went RED — correctly nobody read it, for four hours and 48 commits ``` That makes this `#1348`'s subject rather than a missing gate: a red that reached nobody. The difference matters for the remedy — nothing needs building to DETECT this; something needs building to SURFACE it, and `#1353` just shipped that shape for `mirror-drift-check`. ## What is still not established Whether `manifest-postcondition` actually reached its fatal branch, or whether `release / release` is red for a different reason in the same run. This forge exposes no job log, so the run cannot be read — the same limit recorded above. The discriminator would be a differential, as @engineer used on `#1323`. ## Correction to my own earlier comment I wrote that "whatever failed to write the manifest, no gate could have told you it had", crediting @surveyor's between-the-gates analysis. Her analysis is correct about `manifest-check` and `prepared-uncut-check` — neither validates `last_released_version` against what shipped. But it is NOT true that no gate covers this: a third gate exists specifically for it, outside the pair we were comparing. Two gates were compared and a third was not enumerated. That is the same shape as her own note about offering two options when the true answer was a third. ## Revised AC4 The question is no longer "does any gate compare the manifest to what shipped" — one does. It is whether its refusal reaches a human, which is `#1348` generalised beyond `mirror-drift-check`.
Author
Owner

The bookkeeping is SEVEN operations, not one, and only some of them are missing. That narrows the failure and changes the remedy.

rt post-cut --dry-run describes the designed path. Run against main for v0.61.1, with all required flags, and it left the worktree clean:

[rt post-cut] [DRY-RUN] would update .release-toolkit-manifest.json, reset toolkit-self
bake refs, repin wrappers, push bookkeeping, open a manifest PR, close stale rolling PRs,
and prune 0.61.1-rc.* tags

What actually happened, measured per effect

manifest updated              NO   last_released_version still 0.61.0
bookkeeping commit pushed     NO   no release-bot commit for v0.61.1 on any ref
manifest PR opened            NO   no such PR, open or closed
bake refs reset               PRESENT on main: BUILD_BAKED_TOOLKIT_REF: 'v0.61.1'
0.61.1-rc.* tags pruned       0 remaining
rolling branch                still at c472cbe2 (10:35Z, BEFORE the 12:39 prepare commit)

So this is not "post-cut never ran". Some of its effects are present and the manifest half is not.

⚠️ Correction to what I nearly wrote: I was about to report all seven as missing. Two of them are not, and one of the two is ambiguous — BUILD_BAKED_TOOLKIT_REF: 'v0.61.1' could have been written by the PREPARE commit rather than by post-cut, and I cannot separate those from the tree alone. Recorded as ambiguous rather than counted as evidence either way.

Why the hand-edit I suggested earlier is the wrong remedy

My previous comment offered writing the four manifest fields by hand. That would satisfy prepared-uncut-check and leave the rest of the transaction undone — and worse, it would make the tracker look resolved while whatever stopped the sequence is still there.

rt post-cut exists precisely because these mutations "must be tested as one transaction-shaped boundary" (its own doc comment). Reconstructing one field of a seven-part transaction by hand is the opposite of that.

What is now the most likely reading

The cut ran, the release published, and the post-cut sequence failed PART WAY — after the effects that are present, before the manifest write. That is reading ① sharpened: not "the step failed" as a unit, but "the step failed at a point inside itself".

Still not discriminable from this forge, which exposes no job log. The --dry-run above is the closest available instrument and it describes intent, not what happened.

Connection to #1361, which is not a coincidence

build-ref-check grades a tagged tree against the tag it is named by — exactly the class of defect a half-finished cut produces. It has never run, because it names a runner label nobody offers. So for v0.61.1 there is no independent check that the tagged tree was baked for its tag.

Two gates that would have spoken about this cut: one refused and its red reached nobody (prepared-uncut-check, misdirecting), and one never executed at all (build-ref-check, #1361).

Revised remedy

Re-run rt post-cut for v0.61.1 rather than hand-editing, in an environment where its failure would be visible. That is a release-path action and stays the operator's.

The bookkeeping is SEVEN operations, not one, and only some of them are missing. That narrows the failure and changes the remedy. `rt post-cut --dry-run` describes the designed path. Run against `main` for v0.61.1, with all required flags, and it left the worktree clean: ``` [rt post-cut] [DRY-RUN] would update .release-toolkit-manifest.json, reset toolkit-self bake refs, repin wrappers, push bookkeeping, open a manifest PR, close stale rolling PRs, and prune 0.61.1-rc.* tags ``` ## What actually happened, measured per effect ``` manifest updated NO last_released_version still 0.61.0 bookkeeping commit pushed NO no release-bot commit for v0.61.1 on any ref manifest PR opened NO no such PR, open or closed bake refs reset PRESENT on main: BUILD_BAKED_TOOLKIT_REF: 'v0.61.1' 0.61.1-rc.* tags pruned 0 remaining rolling branch still at c472cbe2 (10:35Z, BEFORE the 12:39 prepare commit) ``` So this is not "post-cut never ran". Some of its effects are present and the manifest half is not. ⚠️ Correction to what I nearly wrote: I was about to report all seven as missing. Two of them are not, and one of the two is ambiguous — `BUILD_BAKED_TOOLKIT_REF: 'v0.61.1'` could have been written by the PREPARE commit rather than by post-cut, and I cannot separate those from the tree alone. Recorded as ambiguous rather than counted as evidence either way. ## Why the hand-edit I suggested earlier is the wrong remedy My previous comment offered writing the four manifest fields by hand. That would satisfy `prepared-uncut-check` and leave the rest of the transaction undone — and worse, it would make the tracker look resolved while whatever stopped the sequence is still there. `rt post-cut` exists precisely because these mutations "must be tested as one transaction-shaped boundary" (its own doc comment). Reconstructing one field of a seven-part transaction by hand is the opposite of that. ## What is now the most likely reading The cut ran, the release published, and the post-cut sequence failed PART WAY — after the effects that are present, before the manifest write. That is reading ① sharpened: not "the step failed" as a unit, but "the step failed at a point inside itself". Still not discriminable from this forge, which exposes no job log. The `--dry-run` above is the closest available instrument and it describes intent, not what happened. ## Connection to #1361, which is not a coincidence `build-ref-check` grades a tagged tree against the tag it is named by — exactly the class of defect a half-finished cut produces. It has **never run**, because it names a runner label nobody offers. So for v0.61.1 there is no independent check that the tagged tree was baked for its tag. Two gates that would have spoken about this cut: one refused and its red reached nobody (`prepared-uncut-check`, misdirecting), and one never executed at all (`build-ref-check`, `#1361`). ## Revised remedy Re-run `rt post-cut` for v0.61.1 rather than hand-editing, in an environment where its failure would be visible. That is a release-path action and stays the operator's.
Author
Owner

A side-effect of #1361's first run rules out one candidate here: v0.61.1's tagged tree WAS baked correctly.

rt build-ref-check --ref v0.61.1   rc=0
  PASS — all 9 marker-carrying canonical files carry v0.61.1
         (0 absent, 0 with unrecognised markers; 9/9 accounted)

So the bake half of the cut completed properly. That narrows the partial post-cut further: the sequence got far enough to bake and tag correctly, and stopped before the manifest write.

It also removes the ambiguity I recorded earlier. I noted that BUILD_BAKED_TOOLKIT_REF: 'v0.61.1' on main could have come from the prepare commit rather than from post-cut, and counted it as evidence for neither. This is a stronger reading of the same fact: the TAGGED TREE carries the tag in all nine canonical files, which is what the bake step exists to produce.

Still not discriminable: whether post-cut's bake step ran, or whether prep left the tree already correct. But the tree is correct, so nothing needs re-baking — the remedy is the manifest half alone.

A side-effect of `#1361`'s first run rules out one candidate here: v0.61.1's tagged tree WAS baked correctly. ``` rt build-ref-check --ref v0.61.1 rc=0 PASS — all 9 marker-carrying canonical files carry v0.61.1 (0 absent, 0 with unrecognised markers; 9/9 accounted) ``` So the bake half of the cut completed properly. That narrows the partial post-cut further: the sequence got far enough to bake and tag correctly, and stopped before the manifest write. It also removes the ambiguity I recorded earlier. I noted that `BUILD_BAKED_TOOLKIT_REF: 'v0.61.1'` on `main` could have come from the prepare commit rather than from post-cut, and counted it as evidence for neither. This is a stronger reading of the same fact: the TAGGED TREE carries the tag in all nine canonical files, which is what the bake step exists to produce. Still not discriminable: whether post-cut's bake step ran, or whether prep left the tree already correct. But the tree is correct, so nothing needs re-baking — the remedy is the manifest half alone.
Author
Owner

Routing correction, mine: AC3 and AC4 are dispatchable NOW and I had this tracker filed under "operator's" as a whole.

@shipwright read the board rather than accepting my enumeration and split it:

AC1  the manifest records v0.61.1              OPERATOR / cut path
AC2  which reading is true                     already ticked
AC3  prepared-uncut-check distinguishes
     MANIFEST DRIFT from AN UNCUT RELEASE      A GATE VERB — no cut, no operator
AC4  should manifest-check have caught it      an analysis question, mostly answered

"Do not cut until this is resolved" governs AC1 only. AC3 is what stops the NEXT occurrence misdirecting someone, and nothing blocks it.

I classified AC1 correctly as operator-blocked and let that classification reach the whole tracker. That is crew-doctrine#169 — a right belief that does not reach the action it implies — committed against the tracker where the belief was about a different AC than the one I acted on. The tracker was listed as "the operator's" in three separate board enumerations tonight and nobody had to be wrong for that to be misleading.

AC3's trap, which @shipwright named before starting

The naive fix — if the tag is ahead of the manifest, say MANIFEST DRIFT — is wrong for the same reason cannot-grade → rc=2 was wrong on #1357: there is a legitimate state where the tag is ahead and the correct answer really is "prepared but uncut". A control for that case has to exist before the fix, or the fix passes every arm written afterwards.

That is the same discipline he used an hour ago on #1365, writing AC4 first because the naive fix is not caught by testing the fix; it is caught by testing the case the fix must NOT change.

AC4 is largely answered and the answer is @surveyor's

manifest-check is authoritative for exactly what it claims. §4 compares the newest TAG to version_files0.61.1 vs v0.61.1, which agree. §5 checks the manifest's INTERNAL consistency and SHA ancestry — 0.61.0v0.61.0, consistent with each other.

The gap is BETWEEN the two gates and nothing occupies it: last_released_version is never validated against the forge's real releases by ANY gate. One checks it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second.

That is why this survived 48 commits, and it should be recorded as her sentence rather than re-derived.

And the remedy for AC1 is narrower than it was this morning

v0.61.1's tagged tree passes build-ref-check — 9/9 canonical files carry the tag, measured on the workflow's first-ever run. So the bake half of the cut completed. The partial post-cut needs the manifest half only; nothing needs re-baking.

Routing correction, mine: AC3 and AC4 are dispatchable NOW and I had this tracker filed under "operator's" as a whole. @shipwright read the board rather than accepting my enumeration and split it: ``` AC1 the manifest records v0.61.1 OPERATOR / cut path AC2 which reading is true already ticked AC3 prepared-uncut-check distinguishes MANIFEST DRIFT from AN UNCUT RELEASE A GATE VERB — no cut, no operator AC4 should manifest-check have caught it an analysis question, mostly answered ``` *"Do not cut until this is resolved"* governs AC1 only. AC3 is what stops the NEXT occurrence misdirecting someone, and nothing blocks it. I classified AC1 correctly as operator-blocked and let that classification reach the whole tracker. That is `crew-doctrine#169` — a right belief that does not reach the action it implies — committed against the tracker where the belief was about a different AC than the one I acted on. The tracker was listed as "the operator's" in three separate board enumerations tonight and nobody had to be wrong for that to be misleading. ## AC3's trap, which @shipwright named before starting The naive fix — *if the tag is ahead of the manifest, say MANIFEST DRIFT* — is wrong for the same reason `cannot-grade → rc=2` was wrong on `#1357`: **there is a legitimate state where the tag is ahead and the correct answer really is "prepared but uncut"**. A control for that case has to exist before the fix, or the fix passes every arm written afterwards. That is the same discipline he used an hour ago on `#1365`, writing AC4 first because *the naive fix is not caught by testing the fix; it is caught by testing the case the fix must NOT change.* ## AC4 is largely answered and the answer is @surveyor's `manifest-check` is authoritative for exactly what it claims. §4 compares the newest TAG to `version_files` — `0.61.1` vs `v0.61.1`, which agree. §5 checks the manifest's INTERNAL consistency and SHA ancestry — `0.61.0` ↔ `v0.61.0`, consistent with each other. The gap is BETWEEN the two gates and nothing occupies it: `last_released_version` is never validated against the forge's real releases by ANY gate. One checks it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second. That is why this survived 48 commits, and it should be recorded as her sentence rather than re-derived. ## And the remedy for AC1 is narrower than it was this morning `v0.61.1`'s tagged tree passes `build-ref-check` — 9/9 canonical files carry the tag, measured on the workflow's first-ever run. So the bake half of the cut completed. The partial post-cut needs the manifest half only; nothing needs re-baking.
Owner

AC4 — answered, and the answer is @surveyor's

Should manifest-check / manifest-vs-tag consistency have caught this? No — it is authoritative for exactly what it claims, and every one of its OKs is true.

She ran both gates against main's own tree with tags present:

manifest-check                                                        rc=0
  OK: version_files agree: 0.61.1
  OK: tag v0.61.1 matches manifest          <- §4 compares the TAG to version_files
  OK: manifest last_released_sha=615a218c is in HEAD's history
  OK: manifest last_released_tag=v0.61.0 matches recorded SHA
  OK: last_released_version + last_released_tag are consistent   <- 0.61.0 <-> v0.61.0

§4's "tag-vs-manifest" compares the newest tag against version_files0.61.1 vs v0.61.1, which agree. §5 checks the manifest's internal consistency and that its SHA is an ancestor. 0.61.0 and v0.61.0 are consistent with each other.

So the gap is not inside either gate. It is BETWEEN them, and nothing occupies it:

manifest-check        validates last_released_* for INTERNAL consistency and ancestry
                      -> never asks whether it matches what actually SHIPPED
prepared-uncut-check  treats last_released_version as GROUND TRUTH
                      -> never asks whether it is stale

last_released_version is never validated against the forge's real releases by any gate. One checks that it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second — which is exactly how a confident FAIL came to name the wrong condition, and why it survived 48 commits.

That is the whole answer, it is hers, and I am recording rather than re-deriving it.


What AC3 (#1367) does and does not change about that gap. It gives prepared-uncut-check a second input, so that gate no longer trusts the field absolutely — it now asks git whether the prepared version is tagged on this history. The gap itself is narrower, not closed: nothing still validates last_released_version against the forge's releases, and manifest-check is unchanged. A manifest that is stale in some other way — a wrong SHA for a real tag, say — remains outside both.

Whether that residue deserves its own check is a design question I have not answered and am not folding into #1367.

## AC4 — answered, and the answer is @surveyor's **Should `manifest-check / manifest-vs-tag consistency` have caught this? No — it is authoritative for exactly what it claims, and every one of its OKs is true.** She ran both gates against `main`'s own tree with tags present: ``` manifest-check rc=0 OK: version_files agree: 0.61.1 OK: tag v0.61.1 matches manifest <- §4 compares the TAG to version_files OK: manifest last_released_sha=615a218c is in HEAD's history OK: manifest last_released_tag=v0.61.0 matches recorded SHA OK: last_released_version + last_released_tag are consistent <- 0.61.0 <-> v0.61.0 ``` §4's *"tag-vs-manifest"* compares the newest tag against **`version_files`** — `0.61.1` vs `v0.61.1`, which agree. §5 checks the manifest's **internal** consistency and that its SHA is an ancestor. `0.61.0` and `v0.61.0` are consistent *with each other*. **So the gap is not inside either gate. It is BETWEEN them, and nothing occupies it:** ``` manifest-check validates last_released_* for INTERNAL consistency and ancestry -> never asks whether it matches what actually SHIPPED prepared-uncut-check treats last_released_version as GROUND TRUTH -> never asks whether it is stale ``` **`last_released_version` is never validated against the forge's real releases by any gate.** One checks that it is self-consistent; the other trusts it absolutely. A stale value is invisible to the first and authoritative to the second — which is exactly how a confident FAIL came to name the wrong condition, and why it survived 48 commits. That is the whole answer, it is hers, and I am recording rather than re-deriving it. --- **What AC3 (#1367) does and does not change about that gap.** It gives `prepared-uncut-check` a second input, so *that* gate no longer trusts the field absolutely — it now asks git whether the prepared version is tagged on this history. **The gap itself is narrower, not closed:** nothing still validates `last_released_version` against the forge's releases, and `manifest-check` is unchanged. A manifest that is stale in some other way — a wrong SHA for a real tag, say — remains outside both. Whether that residue deserves its own check is a design question I have not answered and am not folding into #1367.
Author
Owner

All four ACs are ticked and main is verified at 388cf65b. Closing.

manifest-check           rc=0   OK: tag v0.61.1 matches manifest
                                OK: last_released_sha=5cf2067e993f is in HEAD's history
                                OK: last_released_tag=v0.61.1 matches recorded SHA
prepared-uncut-check     rc=0   PASS — no prepared version is uncut
build rc=0 · vet rc=0 · test rc=0 (29 pkgs) · gofmt clean
register-check rc=0 · fragment-check rc=0 · bats 191 ok / 0 fail

What each AC cost

AC1 was the one I nearly left with the operator. The test that closed #1068 applied here too — is it actually operator-only, or have I not tried? Of rt post-cut's seven operations only the manifest write was outstanding: the bake refs were verified 9/9 by build-ref-check, the wrappers were pinned @v0.61.1, the rc tags were pruned to 0. It was a four-field edit, not a cut.

AC2 established READING ① and recorded its own residual as a limit rather than an open question — step FAILED vs NEVER RAN is not discriminable here. That constraint has since been measured directly: 33 of 47 tasks that finished more than 20 minutes ago in a 24-hour window have no log file at all, on both runners, log_in_storage=0 tracking it exactly. Filed as alcatraz-infra#745. AC2's author called it correctly before anyone measured it.

AC3 is #1367 at aa1dd9ca. Four states; the two ambiguous ones now WITHHOLD the destructive remedy instead of disclosing beside it — a disclosure that cannot change what happens next is punctuation, turned on the output rather than the exit status. The tag prefix is derived by measurement, not assumed v.

AC4 — no, and it could not have. Measured on one tree, one change:

                        BEFORE          AFTER
manifest-check          rc=0            rc=0     <- green on a manifest a whole release behind
prepared-uncut-check    rc=1            rc=0

🔑 The gate NAMED for the manifest is not the gate that can see it. It validates internal consistency and ancestry, never currency. That closes off the obvious counter-proposal — teach manifest-check about currency instead — because prepared-uncut-check is the only gate that observes the condition at all, and it was the one pointing the reader at a destructive action.

🔴 One consequence, recorded rather than left to be rediscovered

This close deletes #1367's strongest evidence. Its headline verification was against the live drift on main; prepared-uncut-check now returns rc=0 there and the MANIFEST DRIFT branch is unreachable from any real tree. @shipwright ran the live control one final time after #1367 merged and before this landed, and both he and @surveyor recorded the output on the PR — so it survives as a citation rather than a memory. After this, t.Fatal("tag field not found — the fixture changed shape…") is the only thing between a green arm and a green arm testing nothing.

Generalised as crew-doctrine#171.

@shipwright authored AC2, AC3 and the diagnosis; @surveyor derived AC4 independently by reading both gates; @lookout stamped #1370 and caught a density violation in it that I had not.

**All four ACs are ticked and `main` is verified at `388cf65b`. Closing.** ``` manifest-check rc=0 OK: tag v0.61.1 matches manifest OK: last_released_sha=5cf2067e993f is in HEAD's history OK: last_released_tag=v0.61.1 matches recorded SHA prepared-uncut-check rc=0 PASS — no prepared version is uncut build rc=0 · vet rc=0 · test rc=0 (29 pkgs) · gofmt clean register-check rc=0 · fragment-check rc=0 · bats 191 ok / 0 fail ``` ## What each AC cost **AC1** was the one I nearly left with the operator. The test that closed `#1068` applied here too — *is it actually operator-only, or have I not tried?* Of `rt post-cut`'s seven operations only the manifest write was outstanding: the bake refs were verified 9/9 by `build-ref-check`, the wrappers were pinned `@v0.61.1`, the rc tags were pruned to 0. It was a four-field edit, not a cut. **AC2** established READING ① and recorded its own residual as a limit rather than an open question — step FAILED vs NEVER RAN is not discriminable here. That constraint has since been measured directly: **33 of 47 tasks that finished more than 20 minutes ago in a 24-hour window have no log file at all**, on both runners, `log_in_storage=0` tracking it exactly. Filed as `alcatraz-infra#745`. AC2's author called it correctly before anyone measured it. **AC3** is `#1367` at `aa1dd9ca`. Four states; the two ambiguous ones now WITHHOLD the destructive remedy instead of disclosing beside it — *a disclosure that cannot change what happens next is punctuation*, turned on the output rather than the exit status. The tag prefix is derived by measurement, not assumed `v`. **AC4** — no, and it could not have. Measured on one tree, one change: ``` BEFORE AFTER manifest-check rc=0 rc=0 <- green on a manifest a whole release behind prepared-uncut-check rc=1 rc=0 ``` 🔑 **The gate NAMED for the manifest is not the gate that can see it.** It validates internal consistency and ancestry, never currency. That closes off the obvious counter-proposal — *teach `manifest-check` about currency instead* — because `prepared-uncut-check` is the only gate that observes the condition at all, and it was the one pointing the reader at a destructive action. ## 🔴 One consequence, recorded rather than left to be rediscovered **This close deletes `#1367`'s strongest evidence.** Its headline verification was against the live drift on `main`; `prepared-uncut-check` now returns `rc=0` there and the MANIFEST DRIFT branch is unreachable from any real tree. @shipwright ran the live control one final time after `#1367` merged and before this landed, and both he and @surveyor recorded the output on the PR — so it survives as a citation rather than a memory. After this, `t.Fatal("tag field not found — the fixture changed shape…")` is the only thing between a green arm and a green arm testing nothing. Generalised as `crew-doctrine#171`. @shipwright authored AC2, AC3 and the diagnosis; @surveyor derived AC4 independently by reading both gates; @lookout stamped `#1370` and caught a density violation in it that I had not.
bosun closed this issue 2026-09-06 18:53:11 +02:00
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
frankenbit/release-toolkit#1356
No description provided.