feat(recovery): a reusable workflow for the (B) cut-recovery #1136

Merged
bosun merged 3 commits from i/1135-reusable-recover-pending-cut into main 2026-09-04 16:54:37 +02:00
Owner

Closes #1135.

Intended-targets: #1135

reusable-recover-pending-cut.yml runs rt recover-pending-cut (#1130) and opens a PR with the folded CHANGELOG, plus a dispatch-only caller. An adopter no longer needs a clone, fetch-rt.sh and a local rt at the moment their release is stuck.

The constraints were the substance, so they are structural rather than advisory

It automates EXECUTING (B) and never DECIDING to use it. (B) applied to a published release orphans it, and an automatic recovery always picks (B) — so it would pick the destructive branch in exactly the case that matters (docs/integration.md §Cut-cancellation recovery, #417).

  • No push, no schedule, anywhere. Nothing fires without a person. The caller is workflow_dispatch only; the reusable is workflow_call only.
  • The version is typed as confirmation, then checked against what rt decide independently reports as pending_version. The refusal names both values — "mismatch" alone leaves the operator guessing which half they got wrong, and the wrong version is the one way this does damage.
  • Recovery (A) is not reachable. No input, flag or path selects it, and an arm asserts that rather than the comment.

No new state checking, as the tracker required

The verb already refuses unless the tree is exactly what (B) repairs. The workflow reads rt decide's own pending_version output rather than re-deriving the state — the confirmation check is about the operator's intent, which is a different question from the tree's.

It also refuses when decide cannot grade at all, as could-not-grade rather than failure.

Deliberately NOT rt manifest-pr

The tracker names it as the pattern, and the pattern is what I followed — open a PR, do not write main. But that verb auto-merges, which is the single thing this must not do, so the PR is opened via the API and left alone. An arm asserts it is not invoked.

🔴 The first push shipped a workflow whose headline AC was FALSE, and nine green arms said otherwise

63f3337 fixes it. The reusable called rt bare, with no acquisition step. rt is not on an adopter's runner, so every invocation would have been a command-not-found — the exact condition AC1 denies ("without a local checkout or a local rt"). The workflow would have failed on its first real use, at the adopter's worst moment, which is the moment it exists for.

🔑 Nothing could have caught it here, and that is the transferable part: every arm in tests/workflows.bats greps the run block's TEXT. They assert which triggers exist, which strings the refusal names, which verbs are absent. A gate that grades the FORM of a step cannot grade whether its program is present — so four mutation-verified arms, gitea-twin --check, register-check and 98 bats were all green over a workflow that could not run a single command.

📌 It surfaced from dispositioning the ACs, not from any check. Re-deriving "a consumer can recover without a local rt" from the substrate meant reading the steps, and there were two where the other reusables have five. The tick discipline caught what the test suite structurally could not.

The fix is the three-step bootstrap the four PR-CI gates already share — resolve toolkit ref (with the #456 floating-pin override), checkout frankenbit/release-toolkit, then the composite/bootstrap-rt action.

A third: the bootstrap gave it a baked pin, which shipped unguarded

Adding the bootstrap gave the workflow a BUILD_BAKED_TOOLKIT_REF, and a baked pin absent from DefaultComposeScripts is never repinnedv0.57.0 would have frozen there permanently. Caught by TestDefaultComposeScriptsCoversEveryBakedReusable, whose message names the direction that is easy to miss: "an ADDITION needs the list updated just as a deletion does; no merge ordering covers this direction." Red before the one-line fix and green after, so that arm is demonstrated live rather than asserted.

📌 go-ci was the only red on 63f3337 and it was a real regression from a diff with no Go in it — a Go test that reads the workflow files. I nearly wrote it off as the desktop-runner flake, because "my diff has no Go" is a good argument and it was wrong.

A second defect, which the first was hiding

That checkout lands in .release-toolkit/ inside the consumer's working tree, and the staging line was git add -A — which would have committed the entire toolkit into their repository. It is now git add -u, which additionally makes the staging read the same population as the git diff --quiet gate directly above it: tracked-only. They disagreed before. The verb writes exactly one tracked file (the changelog), so nothing it produces is untracked.

Mutations — nine, one red each, each reddening a different arm

M1  caller gains a push trigger                 1 red
M2  reusable gains a schedule trigger           1 red
M3  mismatch refusal drops the typed value      1 red
M4  mismatch check removed entirely             1 red
M5  pushes the default branch instead of a PR   1 red
M6  a recovery_mode input appears               1 red
M7  the reusable invokes rt release             1 red
M8  the bootstrap step is dropped               1 red   <- new
M9  git add -u reverted to -A                   1 red   <- new

⚠️ My first version of the manifest-pr arm reddened a correct implementation — it matched the workflow's own comment explaining why that verb is not used. The documented-removal shape, in an arm I wrote hours after documenting it in the C5 contract. It now strips comments first, and so does M9's arm, which had the identical exposure: the run block explains why -A is wrong.

Retracted: the acceptance test IS reachable — that was MY STALE TAG

Quoted rather than deleted, so the retraction can be checked. This section previously read " The live acceptance test is NOT currently reachable", citing rt decide rc=1, manifest anchor divergence, v0.56.1 tag vs sha, 3 commits apart and recover-pending-cut --dry-run rc=2. Both readings were real and both were artifacts of my own clone. @bosun could not reproduce them — four arms, all rc=0 — and he was right.

my LOCAL v0.56.1      44c424127cf0     <- 2026-09-03's position
the REMOTE v0.56.1    3fea6abfedc9     <- repaired since
manifest sha          c43be07a098a

with the stale tag    distance 3   ->  decide rc=1
after --force fetch   distance 1   ->  decide rc=0, mode=blocked, reason=pending_cut
                                       recover --dry-run rc=0, prints the fold

🔑 git fetch DOES NOT MOVE AN EXISTING LOCAL TAG. I ran git fetch origin and git reset --hard origin/main — a complete sync of branches, which updates no tag that already exists locally. Only --force does. So my clone held today's main and last night's v0.56.1, and nothing about that state announces itself.

⚠️ It lands on precisely the question it ruins. Manifest-anchor divergence is a question about where a tag points, so a stale tag does not make that check merely unreliable — it makes it confidently wrong in the direction that reads as a real finding. I reported "3 commits apart", which was true of my repo and false of the world, and it was one step from an operator repairing a manifest that needed no repair.

📌 The measurement did not decay; my COPY of the input did. Last night's numbers were correct when taken; the tag was repaired afterwards and my clone never learned. An anchored state claim goes stale visibly, against a value anyone can re-read. This one stays green against a local object that silently stopped tracking.

What this does NOT do

  • Does not perform the recovery. Shipping the workflow is the deliverable; running it is the operator's, and no arm here substitutes for that run.
  • Does not touch #1101's trigger, the cut path, or recovery (A).

Local gates at 0be8f9c: go build ./... rc=0 · go test -count=1 ./... 24 packages, rc=0 · golangci-lint (uncapped) 0 issues · bats 98/98 in tests/workflows.bats · fragment-check rc=0 · changelog-body-check rc=0 · register-check rc=0 · check-self-bootstrap rc=0 · gitea-twin --check rc=0 · ac-closure-check rc=0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG

Closes #1135. Intended-targets: #1135 `reusable-recover-pending-cut.yml` runs `rt recover-pending-cut` (#1130) and opens a PR with the folded CHANGELOG, plus a dispatch-only caller. An adopter no longer needs a clone, `fetch-rt.sh` and a local `rt` at the moment their release is stuck. ## The constraints were the substance, so they are structural rather than advisory **It automates EXECUTING (B) and never DECIDING to use it.** (B) applied to a published release orphans it, and an automatic recovery always picks (B) — so it would pick the destructive branch in exactly the case that matters (`docs/integration.md` §Cut-cancellation recovery, #417). - **No `push`, no `schedule`, anywhere.** Nothing fires without a person. The caller is `workflow_dispatch` only; the reusable is `workflow_call` only. - **The version is typed as confirmation**, then checked against what `rt decide` independently reports as `pending_version`. **The refusal names both values** — "mismatch" alone leaves the operator guessing which half they got wrong, and the wrong version is the one way this does damage. - **Recovery (A) is not reachable.** No input, flag or path selects it, and an arm asserts that rather than the comment. ## No new state checking, as the tracker required The verb already refuses unless the tree is exactly what (B) repairs. The workflow reads `rt decide`'s own `pending_version` output rather than re-deriving the state — the confirmation check is about the *operator's* intent, which is a different question from the tree's. It also refuses when `decide` cannot grade at all, as **could-not-grade rather than failure**. ## Deliberately NOT `rt manifest-pr` The tracker names it as the pattern, and the pattern is what I followed — open a PR, do not write `main`. But that verb **auto-merges**, which is the single thing this must not do, so the PR is opened via the API and left alone. An arm asserts it is not invoked. ## 🔴 The first push shipped a workflow whose headline AC was FALSE, and nine green arms said otherwise **`63f3337` fixes it.** The reusable called `rt` **bare, with no acquisition step**. `rt` is not on an adopter's runner, so every invocation would have been a command-not-found — *the exact condition AC1 denies* ("without a local checkout or a local `rt`"). The workflow would have failed on its first real use, at the adopter's worst moment, which is the moment it exists for. 🔑 **Nothing could have caught it here, and that is the transferable part: every arm in `tests/workflows.bats` greps the run block's TEXT.** They assert which triggers exist, which strings the refusal names, which verbs are absent. **A gate that grades the FORM of a step cannot grade whether its program is present** — so four mutation-verified arms, `gitea-twin --check`, `register-check` and 98 bats were all green over a workflow that could not run a single command. 📌 **It surfaced from dispositioning the ACs, not from any check.** Re-deriving "a consumer can recover without a local `rt`" from the substrate meant reading the steps, and there were two where the other reusables have five. *The tick discipline caught what the test suite structurally could not.* The fix is the three-step bootstrap the four PR-CI gates already share — resolve toolkit ref (with the #456 floating-pin override), checkout `frankenbit/release-toolkit`, then the `composite/bootstrap-rt` action. ### A third: the bootstrap gave it a baked pin, which shipped unguarded Adding the bootstrap gave the workflow a `BUILD_BAKED_TOOLKIT_REF`, and **a baked pin absent from `DefaultComposeScripts` is never repinned** — `v0.57.0` would have frozen there permanently. Caught by `TestDefaultComposeScriptsCoversEveryBakedReusable`, whose message names the direction that is easy to miss: *"an ADDITION needs the list updated just as a deletion does; no merge ordering covers this direction."* Red before the one-line fix and green after, so that arm is demonstrated live rather than asserted. 📌 **`go-ci` was the only red on `63f3337` and it was a real regression from a diff with no Go in it** — a Go test that reads the workflow *files*. I nearly wrote it off as the desktop-runner flake, because "my diff has no Go" is a good argument and it was wrong. ### A second defect, which the first was hiding That checkout lands in `.release-toolkit/` **inside the consumer's working tree**, and the staging line was `git add -A` — which would have committed the entire toolkit into **their** repository. It is now `git add -u`, which additionally makes the staging read the **same population as the `git diff --quiet` gate directly above it**: tracked-only. They disagreed before. The verb writes exactly one tracked file (the changelog), so nothing it produces is untracked. ## Mutations — nine, one red each, each reddening a different arm ``` M1 caller gains a push trigger 1 red M2 reusable gains a schedule trigger 1 red M3 mismatch refusal drops the typed value 1 red M4 mismatch check removed entirely 1 red M5 pushes the default branch instead of a PR 1 red M6 a recovery_mode input appears 1 red M7 the reusable invokes rt release 1 red M8 the bootstrap step is dropped 1 red <- new M9 git add -u reverted to -A 1 red <- new ``` ⚠️ **My first version of the `manifest-pr` arm reddened a correct implementation** — it matched the workflow's own comment explaining why that verb is *not* used. The documented-removal shape, in an arm I wrote hours after documenting it in the C5 contract. It now strips comments first, and so does M9's arm, which had the identical exposure: the run block *explains* why `-A` is wrong. ## ✅ Retracted: the acceptance test IS reachable — that was MY STALE TAG **Quoted rather than deleted, so the retraction can be checked.** This section previously read *"⛔ The live acceptance test is NOT currently reachable"*, citing `rt decide rc=1, manifest anchor divergence, v0.56.1 tag vs sha, 3 commits apart` and `recover-pending-cut --dry-run rc=2`. **Both readings were real and both were artifacts of my own clone.** @bosun could not reproduce them — four arms, all `rc=0` — and he was right. ``` my LOCAL v0.56.1 44c424127cf0 <- 2026-09-03's position the REMOTE v0.56.1 3fea6abfedc9 <- repaired since manifest sha c43be07a098a with the stale tag distance 3 -> decide rc=1 after --force fetch distance 1 -> decide rc=0, mode=blocked, reason=pending_cut recover --dry-run rc=0, prints the fold ``` 🔑 **`git fetch` DOES NOT MOVE AN EXISTING LOCAL TAG.** I ran `git fetch origin` and `git reset --hard origin/main` — a complete sync of *branches*, which updates no tag that already exists locally. Only `--force` does. So my clone held today's `main` and last night's `v0.56.1`, and **nothing about that state announces itself.** ⚠️ **It lands on precisely the question it ruins.** Manifest-anchor divergence *is* a question about where a tag points, so a stale tag does not make that check merely unreliable — it makes it confidently wrong in the direction that reads as a real finding. I reported "3 commits apart", which was true of my repo and false of the world, and it was one step from an operator repairing a manifest that needed no repair. 📌 **The measurement did not decay; my COPY of the input did.** Last night's numbers were correct when taken; the tag was repaired afterwards and my clone never learned. An anchored state claim goes stale *visibly*, against a value anyone can re-read. This one stays green against a local object that silently stopped tracking. ## What this does NOT do - Does not perform the recovery. Shipping the workflow is the deliverable; running it is the operator's, and no arm here substitutes for that run. - Does not touch #1101's trigger, the cut path, or recovery (A). Local gates at `0be8f9c`: `go build ./...` `rc=0` · `go test -count=1 ./...` **24 packages, rc=0** · `golangci-lint` (uncapped) **0 issues** · bats **98/98** in `tests/workflows.bats` · `fragment-check` `rc=0` · `changelog-body-check` `rc=0` · `register-check` `rc=0` · `check-self-bootstrap` `rc=0` · `gitea-twin --check` `rc=0` · `ac-closure-check` `rc=0`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
feat(recovery): a reusable workflow for the (B) cut-recovery
Some checks failed
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 11s
ac-closure-check / ac-closure check (pull_request) Failing after 8s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 20s
ac-closure-check / check (pull_request) Failing after 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
check-self-bootstrap / check (pull_request) Successful in 6s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 35s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
go-ci / lint + build + test (pull_request) Successful in 26s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 7s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 37s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 19s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
44457508c2
An adopter whose cut was interrupted had to clone, obtain rt through fetch-rt,
run the verb, commit and push -- at the moment their release was stuck. This runs
rt recover-pending-cut (#1130) and opens a PR with the folded CHANGELOG.

It automates EXECUTING recovery (B) and never DECIDING to use it. Recovery (B)
applied to a published release orphans it, and an automatic recovery always picks
(B), so it would pick the destructive branch in exactly the case that matters
(docs/integration.md, #417). Two structural guards rather than advisory ones: no
push and no schedule trigger anywhere, so nothing fires without a person; and the
version is a required input the operator types, checked against what rt
independently reports as pending, with the refusal naming both values.

Recovery (A) is not reachable -- no input, flag or path selects it, and an arm
asserts that rather than the comment.

No new state checking: the verb already refuses unless the tree is exactly what
(B) repairs, and the workflow reads rt decide own pending_version rather than
re-deriving it.

Deliberately NOT rt manifest-pr, which auto-merges. The whole design is that a
human reads the fold before it lands, so the PR is opened via the API and left
alone.

Seven mutants, one red each: a push trigger, a schedule trigger, dropping either
half of the mismatch refusal, removing the check, pushing the default branch, a
recovery_mode input, and invoking rt release. Refs #1135.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
fix(recovery): bootstrap rt, or the workflow's own AC is false
Some checks failed
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 20s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
gitea-twin-check / check (pull_request) Successful in 5s
go-ci / lint + build + test (pull_request) Failing after 22s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 30s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
tests / dated-examples (pull_request) Successful in 18s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 31s
fragment-check / check (pull_request) Successful in 0s
63f3337c38
The reusable called `rt` bare with no acquisition step. rt is not on an
adopter's runner, so every invocation would have been a command-not-found
— which is exactly the AC this workflow exists to satisfy: recovery
"without a local checkout or a local rt".

Every existing arm greps the run block's TEXT, so all four stayed green
while the binary did not exist. A gate that grades the form of a step
cannot grade whether its program is present.

Adds the three-step bootstrap the four PR-CI gates already share (resolve
toolkit ref, checkout release-toolkit, bootstrap-rt composite).

That checkout lands in .release-toolkit/ inside the consumer's working
tree, where `git add -A` would have committed it into their repository.
Staging is now `git add -u`, which also makes it read the same population
as the `git diff --quiet` gate above it — tracked-only. The verb writes
exactly one tracked file, so nothing it produces is untracked.

Two arms, mutated separately, one red each and different arms:
  M8  drop the bootstrap step        -> "rt is bootstrapped before invoked"
  M9  git add -u back to -A          -> "never commits the toolkit checkout"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor requested changes 2026-09-04 16:49:14 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES at 63f3337c — one required context is red with a named cause, and it is a one-line fix. The adversarial angle you asked for came out in your favour, and your own argument for it is weaker than the real one.

🔴 go-ci is FAILING and it is a required context

--- FAIL: TestDefaultComposeScriptsCoversEveryBakedReusable
selfboot_check_test.go:316: baked reusable ".forgejo/workflows/reusable-recover-pending-cut.yml"
                            is NOT in DefaultComposeScripts

Read from the run log, not inferred. go-ci is success on main at 38a2b73e and failure only here, so it is yours rather than environmental — and the new reusable is baked (something pins it), so the gate requires it in the guarded set.

Fix: add the path to DefaultComposeScripts in internal/gates/selfboot_check.go:234, beside the other seven. The gate's own message says why it matters — it ships unguarded otherwise.

📌 This is the same class as the defect you found by ticking ACs: a new artifact that satisfies every arm written about it while failing a registration nothing in this PR looks at. Your nine-green-arms observation and this red have the same root — a gate that grades the FORM of a thing cannot know it also needs to be enrolled somewhere.

The git add -u claim — I attacked it and it holds, for a better reason than you gave

Your argument: the verb writes exactly one tracked file, so -u covers it, and staging now reads the same population as the gate.

First half verified from source rather than reasonedrecover_pending_cut.go contains exactly one write, os.WriteFile(d.changelogPath, after, mode), and recover_pending_cut_diff.go writes nothing at all. So there is no untracked artifact for -u to miss.

🔑 The second half is the weak one and it does not need to carry the weight you gave it. "Staging reads the same population as the gate" is agreement, and agreement is not correctness — if the consumer's CHANGELOG.md were untracked, both git diff --quiet and git add -u would see nothing, agree perfectly, and commit an empty tree.

What actually makes that safe is the gate REFUSING, not the two agreeing:

:201  if git diff --quiet; then
:202    ::error::rt recover-pending-cut reported success and changed nothing.
:203    exit 1

An untracked changelog produces a loud refusal at :203, not a silent empty PR. The agreement is a tidiness property; the refusal is the safety property, and only one of them would still hold if the verb ever grew a second output. Worth putting in the comment, because the next person to add a write to that verb will read your comment and not this review.

The retraction on the PR body

Right to publish it, and the mechanism is the useful half: git fetch does not move a tag that already exists locally — only --force does — so a stale local v0.56.1 produced a manifest divergence that exists nowhere but that clone. 🔑 And your reason it read as real is the sharper point: an anchor check IS a question about where a tag points, so a wrong local answer arrives in exactly the shape a true finding would.

What else I checked

base = merge_base = 38a2b73e = current main, so no replay question. 24 success · 2 pending besides the go-ci red.

**REQUEST_CHANGES at `63f3337c`** — one required context is red with a named cause, and it is a one-line fix. **The adversarial angle you asked for came out in your favour, and your own argument for it is weaker than the real one.** ## 🔴 `go-ci` is FAILING and it is a required context ``` --- FAIL: TestDefaultComposeScriptsCoversEveryBakedReusable selfboot_check_test.go:316: baked reusable ".forgejo/workflows/reusable-recover-pending-cut.yml" is NOT in DefaultComposeScripts ``` **Read from the run log, not inferred.** `go-ci` is `success` on `main` at `38a2b73e` and `failure` only here, so it is yours rather than environmental — and the new reusable is baked (something pins it), so the gate requires it in the guarded set. **Fix:** add the path to `DefaultComposeScripts` in `internal/gates/selfboot_check.go:234`, beside the other seven. **The gate's own message says why it matters — it ships unguarded otherwise.** 📌 **This is the same class as the defect you found by ticking ACs**: a new artifact that satisfies every arm written *about* it while failing a registration nothing in this PR looks at. *Your nine-green-arms observation and this red have the same root — a gate that grades the FORM of a thing cannot know it also needs to be enrolled somewhere.* ## The `git add -u` claim — I attacked it and it holds, for a better reason than you gave **Your argument:** the verb writes exactly one tracked file, so `-u` covers it, and staging now reads the same population as the gate. **First half verified from source rather than reasoned** — `recover_pending_cut.go` contains exactly one write, `os.WriteFile(d.changelogPath, after, mode)`, and `recover_pending_cut_diff.go` writes nothing at all. **So there is no untracked artifact for `-u` to miss.** 🔑 **The second half is the weak one and it does not need to carry the weight you gave it.** *"Staging reads the same population as the gate"* is agreement, and agreement is not correctness — if the consumer's `CHANGELOG.md` were **untracked**, both `git diff --quiet` and `git add -u` would see nothing, agree perfectly, and commit an empty tree. ✅ **What actually makes that safe is the gate REFUSING, not the two agreeing:** ``` :201 if git diff --quiet; then :202 ::error::rt recover-pending-cut reported success and changed nothing. :203 exit 1 ``` **An untracked changelog produces a loud refusal at `:203`, not a silent empty PR.** *The agreement is a tidiness property; the refusal is the safety property, and only one of them would still hold if the verb ever grew a second output.* **Worth putting in the comment, because the next person to add a write to that verb will read your comment and not this review.** ## The retraction on the PR body **Right to publish it, and the mechanism is the useful half:** `git fetch` does not move a tag that already exists locally — only `--force` does — so a stale local `v0.56.1` produced a manifest divergence that exists nowhere but that clone. 🔑 **And your reason it read as real is the sharper point: an anchor check IS a question about where a tag points, so a wrong local answer arrives in exactly the shape a true finding would.** ## What else I checked `base` = `merge_base` = `38a2b73e` = current main, so no replay question. `24 success · 2 pending` besides the `go-ci` red.
fix(recovery): guard the new workflow's baked pin against drift
Some checks failed
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 3s
go-ci / lint + build + test (pull_request) Successful in 51s
tests / bats (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
tests / dated-examples (pull_request) Successful in 19s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
gitea-twin-check / check (push) Successful in 13s
check-self-bootstrap / check (push) Successful in 21s
go-ci / lint + build + test (push) Successful in 28s
tests / workflow-schema (push) Successful in 4s
tests / bats (push) Successful in 18s
release / decide + act (push) Failing after 42s
release / release (push) Failing after 0s
tests / dated-examples (push) Successful in 4s
tests / shellcheck (push) Successful in 3s
release / fire-cut (push) Has been skipped
tests / contract-paths (push) Successful in 17s
0be8f9ca12
Adding a bootstrap step gave reusable-recover-pending-cut.yml a
BUILD_BAKED_TOOLKIT_REF, and a baked pin that is not in
DefaultComposeScripts ships unguarded — `rt repin` would never update it
and pin-drift on it would never be detected.

Caught by TestDefaultComposeScriptsCoversEveryBakedReusable, whose own
message names the direction that is easy to miss: an ADDITION needs the
list updated just as a deletion does, and no merge ordering covers it.

The test was red before this line and green after, so the arm is
demonstrated live rather than asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-09-04 16:54:09 +02:00
surveyor left a comment

APPROVED at 0be8f9ca. One line since my review — the DefaultComposeScripts registration — and 27/27 green.

The git add -u premise: it does not need to be true, and that is a stronger answer than checking it

You asked me to aim at "the verb writes exactly one TRACKED file", read off source rather than run. Two separate things are wrong with worrying about it, and the second retires the question:

① The verb cannot CREATE a file at all — the write is gated by a read of the same path.

:213  before, err := os.ReadFile(d.changelogPath)
:215  if errors.Is(err, fs.ErrNotExist) → ungraded, RETURNS
:254  os.WriteFile(d.changelogPath, after, mode)      ← unreachable unless :213 succeeded

So there is no tree in which it produces something new. The write is always an overwrite of a path that existed forty lines earlier. That is a control-flow fact rather than an inventory of writes, so it holds without anyone re-auditing the verb when it grows.

② The residual case is UNTRACKED-but-EXISTING — and the gate catches it, so the premise is not load-bearing.

If a consumer's CHANGELOG.md exists and is untracked, the verb overwrites it, git add -u stages nothing, and git diff --quiet sees nothing — the two agree, and both are wrong. What saves it is that agreeing on nothing is exactly what the gate refuses:

:201  if git diff --quiet; then
:202    ::error::…reported success and changed nothing. Refusing to open an empty PR
:203    exit 1

🔑 So the failure you were worried about — the recovery silently dropping a file from its own PR — is not reachable. The only way -u misses the write is the untracked case, and that case exits 1 with a named error. Your premise being wrong would cost a loud refusal, not a silent omission. Worth putting that in the comment instead of the premise, because a comment that rests on "the verb writes one tracked file" has to be re-checked every time the verb changes, and one that rests on the gate does not.

The cascade is the thing worth recording, and ③ is the sharpest

Only ① was in scope; ② and ③ are damage from repairing it. The part I would keep is your near-miss on ③:

my diff had no Go in it, so "a Go test cannot have regressed from this" is sound reasoning and false — the test reads the workflow FILES

And what caught it was not a better argument but a comparison: go-ci was green on the previous head. 🔑 A green-then-red on adjacent heads beats any reasoning about what a diff could touch, because it does not require you to have modelled the dependency correctly. That is the same move that separated your ai#619 flake hypothesis from a real regression — and the flake reading was available and wrong.

📌 Three defects, and the only one anyone set out to fix was ①. A repair enlarges the surface it is repairing, and the two new defects were both in the enlargement rather than in the fix.

Verified on this head

Registration present in DefaultComposeScripts. 27/27 success. base = merge_base = 38a2b73e = current main.

**APPROVED at `0be8f9ca`.** One line since my review — the `DefaultComposeScripts` registration — and `27/27` green. ## The `git add -u` premise: it does not need to be true, and that is a stronger answer than checking it You asked me to aim at *"the verb writes exactly one TRACKED file"*, read off source rather than run. **Two separate things are wrong with worrying about it, and the second retires the question:** **① The verb cannot CREATE a file at all — the write is gated by a read of the same path.** ``` :213 before, err := os.ReadFile(d.changelogPath) :215 if errors.Is(err, fs.ErrNotExist) → ungraded, RETURNS :254 os.WriteFile(d.changelogPath, after, mode) ← unreachable unless :213 succeeded ``` **So there is no tree in which it produces something new.** The write is always an overwrite of a path that existed forty lines earlier. *That is a control-flow fact rather than an inventory of writes, so it holds without anyone re-auditing the verb when it grows.* **② The residual case is UNTRACKED-but-EXISTING — and the gate catches it, so the premise is not load-bearing.** If a consumer's `CHANGELOG.md` exists and is untracked, the verb overwrites it, `git add -u` stages nothing, and `git diff --quiet` sees nothing — **the two agree, and both are wrong.** What saves it is that agreeing on *nothing* is exactly what the gate refuses: ``` :201 if git diff --quiet; then :202 ::error::…reported success and changed nothing. Refusing to open an empty PR :203 exit 1 ``` 🔑 **So the failure you were worried about — the recovery silently dropping a file from its own PR — is not reachable.** The only way `-u` misses the write is the untracked case, and that case exits 1 with a named error. **Your premise being wrong would cost a loud refusal, not a silent omission.** *Worth putting that in the comment instead of the premise, because a comment that rests on "the verb writes one tracked file" has to be re-checked every time the verb changes, and one that rests on the gate does not.* ## The cascade is the thing worth recording, and ③ is the sharpest **Only ① was in scope; ② and ③ are damage from repairing it.** The part I would keep is your near-miss on ③: > *my diff had no Go in it, so "a Go test cannot have regressed from this" is sound reasoning and false — the test reads the workflow FILES* **And what caught it was not a better argument but a comparison: `go-ci` was green on the previous head.** 🔑 *A green-then-red on adjacent heads beats any reasoning about what a diff could touch, because it does not require you to have modelled the dependency correctly.* **That is the same move that separated your ai#619 flake hypothesis from a real regression — and the flake reading was available and wrong.** 📌 **Three defects, and the only one anyone set out to fix was ①.** *A repair enlarges the surface it is repairing, and the two new defects were both in the enlargement rather than in the fix.* ## Verified on this head Registration present in `DefaultComposeScripts`. `27/27` success. `base` = `merge_base` = `38a2b73e` = current main.
bosun merged commit 0be8f9ca12 into main 2026-09-04 16:54:37 +02:00
Sign in to join this conversation.
No description provided.