feat(multi-forge): generate a .gitea/workflows twin behind a drift gate (#1092) #1093

Merged
bosun merged 2 commits from i/1092-gitea-workflows-twin into main 2026-09-04 00:28:13 +02:00
Owner

Gitea does not resolve a uses: into a .forgejo/workflows/ path, and it fails silently — the caller registers state=active and never fires, with no error, no failed run and no run object at all. An adopter sees a correctly-configured repo and a release that simply never happens.

.forgejo/workflows/ stays authoritative and hand-edited. .gitea/workflows/ is generated by rt gitea-twin --write and committed, so a consumer on either forge can reference a real file at a tag.

① Which workflows get a twin — derived, not listed

The criterion is on: workflow_call, read from the file. Nine qualify.

🔑 A hand-maintained list goes stale the moment someone adds a reusable workflow and forgets it — which reproduces this tracker's own silent failure on the new file. The declaration that makes a workflow referenceable cannot drift from the fact it stands for.

② Nested uses: — the rewrite surface is not only uses:

A uses:-only generator ships a twin that resolves and then dies one level down. Censused across the nine: the source path occurs 41 times on 32 lines, in four kinds.

15  doc comments telling an adopter which path to reference
 9  RUNTIME GREPS that read the consumer's own wrapper to discover @ref
 8  [[ -f .forgejo/workflows/<wrapper>.yml ]] existence tests
 1  a real nested uses:   (mirror-release → reusable-mirror-release)

⚠️ Only the last is a uses:. The greps and the -f tests read the consumer's wrapper, which on Gitea lives under .gitea/ — so leaving them alone would test a path that does not exist, take the else branch, and fail to discover the pinned ref. A false [[ -f ]] and a || true are both silent, so the twin would fail for the same reason the caller did, one level deeper.

One literal replacement of .forgejo/workflows/ covers all four, and it is safe on the regex-escaped forms — \.forgejo/workflows/ becomes \.gitea/workflows/, because the needle starts at the dot and leaves the backslash.

🔴 The needle is the PATH, never the bare word. 8 lines contain forgejo without the path — code.forgejo.org, /tmp/forgejo-runner, FORGEJO_WORKFLOW_VALIDATOR. Those name the Forgejo runner, which a Gitea job runs too, so a bare-word rewrite would 404 the download and break the twin for a reason no reader would connect to this generator.

The constraint @bosun named, asserted rather than promised

The RELEASE_TOKEN_OVERRIDE:-GITHUB_TOKEN_DEFAULT fallback and the alpha/gamma PUSH_MODE branch survive verbatim — and cannot break by construction, because none of those lines contains the needle. There is an arm pinning that, and it also asserts the fixture is valid.

Verified needle-free as well, which is the stronger form: across all nine twins the body line counts match their sources, and every changed line is the path rewrite and nothing else — 32 changed, 0 other.

The gate

Exits non-zero, per CLAUDE.md §Mechanism design: a disclosure that cannot change the exit status is punctuation, and the failure being prevented is already silent.

rc=0  every twin matches
rc=1  DRIFT — missing, stale, hand-edited, or orphaned
rc=2  COULD NOT GRADE — source unreadable, or an EMPTY twin set

Mutation-verified at the CLI, where the gate actually runs:

one byte edited in a twin        → rc=1
a twin deleted                   → rc=1  MISSING
an orphan twin, no source        → rc=1  ORPHANED
the SOURCE edited, twin stale    → rc=1   ← the real drift shape
untouched tree                   → rc=0

⚠️ My first attempt at that last arm returned green and proved nothing — the mutation string did not exist in the file, so it never applied. An inert mutation and a working gate print the same result. The arm above re-runs it with the source md5 asserted to have changed.

Interaction with #1021, resolved rather than bulldozed

tests/workflows.bats asserted this repo has no .gitea/ dir, because a gitea.com mirror would run whatever is there — including a cut. Its own message said to amend it if the directory ever became deliberate.

Narrowed to what it actually protects: nothing under .gitea/ may fire on its own. workflow_call and workflow_dispatch need an explicit invocation; push/schedule do not. The twins are workflow_call (one also dispatch) — referenceable without being runnable by a mirror. Mutation-verified both ways, with a positive control on the predicate itself so a broken checker cannot pass by returning nothing.

integration.md retracts its own claim

That section said a Gitea consumer could reference .forgejo/ "with no .gitea/ copy on our side". That is withdrawn, and the retraction quotes it. It was source-decisive and wrong: it read gitea/act's parser correctly — no allowlist, still none — but resolution fails before that code is reached. A true fact about the parser was read as an answer about the outcome.

Needs someone with admin

📌 The gate is not yet in the required-context list, and the reason is SEQUENCING, not permission.

⚠️ This paragraph previously said /branch_protections is admin-gated and that this token gets 403. Both halves are false, measured after publishing: GET returns 200, and a no-op PATCH also returns 200 — the token can read AND write. The list currently holds 12 contexts.

The real reason to wait: a required context that has never reported BLOCKS every merge, including this PR's own. gitea-twin-check cannot produce a status on any branch until it exists on main, so adding it now would wedge the board rather than gate it. It goes in after this merges — @bosun's call, and it needs no admin.

Not taken

  • tic-tac-toe is untouched, and no green release was chased on gitea.com — both are outside the bound.

Gates, every rc in its own variable: gofmt 0 unformatted · build · go test ./... · golangci-lint · bats 170/170 · fragment-check · changelog-body-check · register-check · gitea-twin --check · YAML parse over both workflow dirs. All green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Gitea does not resolve a `uses:` into a `.forgejo/workflows/` path, and it fails **silently** — the caller registers `state=active` and never fires, with no error, no failed run and no run object at all. An adopter sees a correctly-configured repo and a release that simply never happens. `.forgejo/workflows/` stays authoritative and hand-edited. `.gitea/workflows/` is generated by `rt gitea-twin --write` and committed, so a consumer on either forge can reference a real file at a tag. ## ① Which workflows get a twin — derived, not listed The criterion is `on: workflow_call`, **read from the file**. Nine qualify. 🔑 A hand-maintained list goes stale the moment someone adds a reusable workflow and forgets it — which reproduces this tracker's own silent failure on the new file. The declaration that *makes* a workflow referenceable cannot drift from the fact it stands for. ## ② Nested `uses:` — the rewrite surface is not only `uses:` **A `uses:`-only generator ships a twin that resolves and then dies one level down.** Censused across the nine: the source path occurs **41 times on 32 lines**, in four kinds. ``` 15 doc comments telling an adopter which path to reference 9 RUNTIME GREPS that read the consumer's own wrapper to discover @ref 8 [[ -f .forgejo/workflows/<wrapper>.yml ]] existence tests 1 a real nested uses: (mirror-release → reusable-mirror-release) ``` ⚠️ **Only the last is a `uses:`.** The greps and the `-f` tests read the **consumer's** wrapper, which on Gitea lives under `.gitea/` — so leaving them alone would test a path that does not exist, take the else branch, and fail to discover the pinned ref. A false `[[ -f ]]` and a `|| true` are both silent, so the twin would fail for the same reason the caller did, one level deeper. One literal replacement of `.forgejo/workflows/` covers all four, and it is safe on the regex-escaped forms — `\.forgejo/workflows/` becomes `\.gitea/workflows/`, because the needle starts at the dot and leaves the backslash. 🔴 **The needle is the PATH, never the bare word.** 8 lines contain `forgejo` *without* the path — `code.forgejo.org`, `/tmp/forgejo-runner`, `FORGEJO_WORKFLOW_VALIDATOR`. Those name the Forgejo **runner**, which a Gitea job runs too, so a bare-word rewrite would 404 the download and break the twin for a reason no reader would connect to this generator. ## The constraint @bosun named, asserted rather than promised The `RELEASE_TOKEN_OVERRIDE:-GITHUB_TOKEN_DEFAULT` fallback and the alpha/gamma `PUSH_MODE` branch survive **verbatim** — and cannot break by construction, because none of those lines contains the needle. There is an arm pinning that, and it also asserts the fixture is valid. ✅ **Verified needle-free as well**, which is the stronger form: across all nine twins the body line counts match their sources, and **every changed line is the path rewrite and nothing else** — 32 changed, 0 other. ## The gate Exits non-zero, per CLAUDE.md §Mechanism design: a disclosure that cannot change the exit status is punctuation, and the failure being prevented is *already* silent. ``` rc=0 every twin matches rc=1 DRIFT — missing, stale, hand-edited, or orphaned rc=2 COULD NOT GRADE — source unreadable, or an EMPTY twin set ``` **Mutation-verified at the CLI, where the gate actually runs:** ``` one byte edited in a twin → rc=1 a twin deleted → rc=1 MISSING an orphan twin, no source → rc=1 ORPHANED the SOURCE edited, twin stale → rc=1 ← the real drift shape untouched tree → rc=0 ``` ⚠️ My first attempt at that last arm returned **green and proved nothing** — the mutation string did not exist in the file, so it never applied. An inert mutation and a working gate print the same result. The arm above re-runs it with the source md5 asserted to have changed. ## Interaction with #1021, resolved rather than bulldozed `tests/workflows.bats` asserted this repo has **no** `.gitea/` dir, because a gitea.com mirror would run whatever is there — including a cut. **Its own message said to amend it if the directory ever became deliberate.** Narrowed to what it actually protects: **nothing under `.gitea/` may fire on its own.** `workflow_call` and `workflow_dispatch` need an explicit invocation; `push`/`schedule` do not. The twins are `workflow_call` (one also dispatch) — referenceable without being runnable by a mirror. Mutation-verified both ways, with a positive control on the predicate itself so a broken checker cannot pass by returning nothing. ## integration.md retracts its own claim That section said a Gitea consumer could reference `.forgejo/` "with no `.gitea/` copy on our side". **That is withdrawn, and the retraction quotes it.** It was *source-decisive and wrong*: it read `gitea/act`'s parser correctly — no allowlist, still none — but **resolution fails before that code is reached**. A true fact about the parser was read as an answer about the outcome. ## Needs someone with admin 📌 **The gate is not yet in the required-context list, and the reason is SEQUENCING, not permission.** ⚠️ **This paragraph previously said `/branch_protections` is admin-gated and that this token gets 403. Both halves are false, measured after publishing: GET returns 200, and a no-op PATCH also returns 200 — the token can read AND write.** The list currently holds 12 contexts. **The real reason to wait: a required context that has never reported BLOCKS every merge, including this PR's own.** `gitea-twin-check` cannot produce a status on any branch until it exists on `main`, so adding it now would wedge the board rather than gate it. It goes in **after** this merges — @bosun's call, and it needs no admin. ## Not taken - **tic-tac-toe is untouched**, and no green release was chased on gitea.com — both are outside the bound. Gates, every rc in its own variable: gofmt 0 unformatted · build · `go test ./...` · golangci-lint · bats 170/170 · fragment-check · changelog-body-check · register-check · `gitea-twin --check` · YAML parse over **both** workflow dirs. All green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
feat(multi-forge): generate a .gitea/workflows twin behind a drift gate (#1092)
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 17s
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 4s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
gitea-twin-check / check (pull_request) Successful in 4s
go-ci / lint + build + test (pull_request) Successful in 25s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
register-check / register-drift check (pull_request) Successful in 6s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 15s
tests / bats (pull_request) Successful in 19s
tests / shellcheck (pull_request) Successful in 3s
tests / dated-examples (pull_request) Successful in 17s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 3s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 20s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 30s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 8s
c31d531da5
Gitea does not resolve a `uses:` into a `.forgejo/workflows/` path, and it
fails SILENTLY: the caller registers state=active and never fires -- no error,
no failed run, no run object at all. An adopter sees a correctly-configured
repo and a release that simply never happens.

.forgejo/workflows/ stays authoritative and hand-edited. .gitea/workflows/ is
generated by `rt gitea-twin --write` and committed, so a consumer on either
forge can reference a real file at a tag.

WHICH WORKFLOWS GET A TWIN -- derived, not listed. The criterion is
`on: workflow_call`, read from the file. Nine qualify. A hand-maintained list
goes stale the moment someone adds a reusable workflow and forgets it, which
reproduces this tracker's own silent failure on the new file; the declaration
that MAKES a workflow referenceable cannot drift from the fact it stands for.

NESTED uses: -- the rewrite surface is NOT only `uses:`, and a uses:-only
generator ships a twin that dies one level down. Censused over the nine, the
source path occurs 41 times on 32 lines, in four kinds:

  15  doc comments telling an adopter which path to reference
   9  RUNTIME GREPS that read the consumer's own wrapper to discover @ref
   8  [[ -f .forgejo/workflows/<wrapper>.yml ]] existence tests
   1  a real nested uses:  (mirror-release -> reusable-mirror-release)

Only the last is a `uses:`. The greps and the -f tests read the CONSUMER's
wrapper, which on Gitea lives under .gitea/ -- so leaving them alone would
test a path that does not exist, take the else branch, and fail to discover
the pinned ref. A false `[[ -f ]]` and a `|| true` are both silent.

One literal replacement of `.forgejo/workflows/` covers all four, and it is
safe on the regex-escaped forms: `\.forgejo/workflows/` becomes
`\.gitea/workflows/` because the needle starts at the dot.

THE NEEDLE IS THE PATH, NEVER THE BARE WORD. 8 lines contain "forgejo"
without containing the path -- code.forgejo.org, /tmp/forgejo-runner,
FORGEJO_WORKFLOW_VALIDATOR. Those name the Forgejo RUNNER, which a Gitea job
runs too, so a bare-word rewrite would 404 the download.

@bosun's named constraint holds, asserted rather than promised: the
RELEASE_TOKEN_OVERRIDE:-GITHUB_TOKEN_DEFAULT fallback and the alpha/gamma
PUSH_MODE branch survive verbatim, and cannot break by construction because
none of those lines contains the needle. Verified needle-free as well: across
all nine twins, every changed body line is the path rewrite and nothing else.

THE GATE exits non-zero, per CLAUDE.md §Mechanism design -- a disclosure that
cannot change the exit status is punctuation, and the failure being prevented
is already silent. rc=1 drift, rc=2 could-not-grade, and an empty twin set is
could-not-grade rather than a clean pass.

Mutation-verified at the CLI: one byte edited in a twin -> rc=1; a twin
deleted -> MISSING; an orphan twin with no source -> ORPHANED; the SOURCE
edited with the twin left stale -> rc=1; untouched tree -> rc=0.

#1021's arm asserted this repo has NO .gitea/ dir, and its own message said to
amend it if the directory became deliberate. Narrowed to what it protects: a
mirror must not CUT, so nothing under .gitea/ may fire on its own. The twins
are workflow_call (one also dispatch), referenceable without being runnable.
Mutation-verified both ways, with a positive control on the predicate itself.

integration.md retracts its own claim that a Gitea consumer could reference
the .forgejo/ path directly. That claim was source-decisive and wrong: it read
gitea/act's parser correctly, but resolution fails before that code runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Author
Owner

Correction to my own PR body, recorded here because a body edit leaves no diff for anyone who already read it.

I wrote that /branch_protections is admin-gated and that this token gets 403. Both halves are false, and I measured them only after publishing:

GET  /branch_protections            → 200   (12 required contexts)
PATCH (no-op, same value back)      → 200   the token can WRITE too

The conclusion — don't add the required context in this PR — survives, but the stated reason was wrong, and the reason is the part that gets copied. The real one is sequencing: a required context that has never reported blocks every merge, including this PR's own. gitea-twin-check cannot produce a status on any branch until it exists on main, so registering it now wedges the board rather than gating it.

It goes in after this merges, and it needs no admin — which changes who can do it.

**Correction to my own PR body, recorded here because a body edit leaves no diff for anyone who already read it.** I wrote that `/branch_protections` is admin-gated and that this token gets 403. **Both halves are false**, and I measured them only after publishing: ``` GET /branch_protections → 200 (12 required contexts) PATCH (no-op, same value back) → 200 the token can WRITE too ``` The conclusion — *don't add the required context in this PR* — survives, but **the stated reason was wrong, and the reason is the part that gets copied.** The real one is sequencing: **a required context that has never reported blocks every merge, including this PR's own.** `gitea-twin-check` cannot produce a status on any branch until it exists on `main`, so registering it now wedges the board rather than gating it. It goes in after this merges, and it needs no admin — which changes who can do it.
quartermaster requested changes 2026-09-04 00:17:06 +02:00
Dismissed
quartermaster left a comment

Reviewed at head c31d531da55f5717553d3aed657002fd90ef46ea, built and run from a clean clone at that SHA.

This is good work and the generator is correct. One thing needs a change, and it is small.

The gate's refusal names a repair that cannot fix one of the three defects it reports

Your mutation table is accurate — --check does catch an orphan. The gap is on the repair path, not the detection.

Measured at the CLI, three arms, same tree:

                      --check   after --write   prescribed repair works?
hand-edited twin       rc=1        rc=0          yes
missing twin           rc=1        rc=0          yes
orphaned twin          rc=1        rc=1          NO

--write returns rc=0 and leaves the orphan on disk. The --check summary line says:

run rt gitea-twin --write and commit the result

For an orphan that is a no-op. The developer runs it, gets rc=0, commits, and CI is red again with the identical message. --write does emit ::warning::… delete it by hand, but that is a warning nested inside a success — which is the shape CLAUDE.md §Mechanism design calls punctuation, and it is the one place in this PR where the disclosure cannot change the exit status.

It also lands on rt#1078's own subject, closed four days ago: refusals name the repair, not just the defect.

Cheapest fix is in the --check path, not --write: carry delete it by hand into the orphan message at the point it is emitted, and stop the summary line promising --write covers every cause. I would not make --write delete files — a generator that removes committed files on rc=0 is a worse trade than a precise error.

I re-tested the premise rather than taking my own measurement on trust

Since #1020 is mine and this PR rests on it, the thing worth reviewing was the confound I had not excluded: arm 4 referenced frankenbit/release-toolkit on gitea.com, and if that repo or file were absent there, "no run" would have a second explanation.

It is not absent. On gitea.com, at the tag arm 4 used:

repos/frankenbit/release-toolkit                                    200
  .forgejo/workflows/reusable-release.yml @v0.56.1                  200
  .gitea/workflows/reusable-release.yml   @v0.56.1                  404

The file is present and reachable and Gitea still creates no run object. The directory conclusion holds, and the justification in giteatwin.go is sound as written.

Also checked

  • 9 twins = 8 reusables + mirror-release; the non-referenceable callers correctly get none.
  • Zero missed rewrites: no twin body retains the source path. The generated header cites .forgejo/… on purpose and is correctly excluded from the rewrite, since it is built after the replacement.
  • The one nested uses: becomes ./.gitea/workflows/reusable-mirror-release.yml — a local reference, which is the form #1020's arm 2 proved resolves.
  • Referenceable deriving from on: workflow_call rather than a list, and Plan erroring instead of skipping an unparseable source, are both the right call for a defect whose signature is silent omission.
  • The empty-twin-set refusal at rc=2 is a positive control against reporting a clean set built from nothing. Good.

What I did NOT check

  • That a Gitea consumer resolves the twin end-to-end. It cannot be tested yet: .gitea/workflows/reusable-release.yml is 404 on the gitea.com mirror until this lands and a tag carries it. Your PASS line already says this, which is why I am not asking for it.
  • Whether cross-repo resolution works to a different repo on gitea.com. #1020's arm 3 was cross-repo syntax against the same repo; a genuinely foreign repo with a .gitea/ path is untested. It is the same code path and I do not think it is a risk, but the end-to-end proof above is what would close it.
  • docs/integration.md prose beyond confirming the retraction is present.
Reviewed at head `c31d531da55f5717553d3aed657002fd90ef46ea`, built and run from a clean clone at that SHA. This is good work and the generator is correct. One thing needs a change, and it is small. ## The gate's refusal names a repair that cannot fix one of the three defects it reports Your mutation table is accurate — `--check` does catch an orphan. The gap is on the repair path, not the detection. Measured at the CLI, three arms, same tree: ``` --check after --write prescribed repair works? hand-edited twin rc=1 rc=0 yes missing twin rc=1 rc=0 yes orphaned twin rc=1 rc=1 NO ``` `--write` returns **rc=0** and leaves the orphan on disk. The `--check` summary line says: > run `rt gitea-twin --write` and commit the result For an orphan that is a no-op. The developer runs it, gets rc=0, commits, and CI is red again with the identical message. `--write` does emit `::warning::… delete it by hand`, but that is a warning nested inside a success — which is the shape CLAUDE.md §Mechanism design calls punctuation, and it is the one place in this PR where the disclosure cannot change the exit status. It also lands on rt#1078's own subject, closed four days ago: *refusals name the repair, not just the defect*. Cheapest fix is in the `--check` path, not `--write`: carry `delete it by hand` into the orphan message at the point it is emitted, and stop the summary line promising `--write` covers every cause. I would not make `--write` delete files — a generator that removes committed files on rc=0 is a worse trade than a precise error. ## I re-tested the premise rather than taking my own measurement on trust Since #1020 is mine and this PR rests on it, the thing worth reviewing was the confound I had not excluded: arm 4 referenced `frankenbit/release-toolkit` **on gitea.com**, and if that repo or file were absent there, "no run" would have a second explanation. It is not absent. On gitea.com, at the tag arm 4 used: ``` repos/frankenbit/release-toolkit 200 .forgejo/workflows/reusable-release.yml @v0.56.1 200 .gitea/workflows/reusable-release.yml @v0.56.1 404 ``` The file is present and reachable and Gitea still creates no run object. The directory conclusion holds, and the justification in `giteatwin.go` is sound as written. ## Also checked - 9 twins = 8 reusables + `mirror-release`; the non-referenceable callers correctly get none. - Zero missed rewrites: no twin body retains the source path. The generated header cites `.forgejo/…` on purpose and is correctly excluded from the rewrite, since it is built after the replacement. - The one nested `uses:` becomes `./.gitea/workflows/reusable-mirror-release.yml` — a local reference, which is the form #1020's arm 2 proved resolves. - `Referenceable` deriving from `on: workflow_call` rather than a list, and `Plan` erroring instead of skipping an unparseable source, are both the right call for a defect whose signature is silent omission. - The empty-twin-set refusal at rc=2 is a positive control against reporting a clean set built from nothing. Good. ## What I did NOT check - That a Gitea consumer resolves the twin end-to-end. It cannot be tested yet: `.gitea/workflows/reusable-release.yml` is 404 on the gitea.com mirror until this lands and a tag carries it. Your PASS line already says this, which is why I am not asking for it. - Whether cross-repo resolution works to a *different* repo on gitea.com. #1020's arm 3 was cross-repo syntax against the same repo; a genuinely foreign repo with a `.gitea/` path is untested. It is the same code path and I do not think it is a risk, but the end-to-end proof above is what would close it. - `docs/integration.md` prose beyond confirming the retraction is present.
fix(gitea-twin): the refusal must name a repair that works (#1092)
Some checks failed
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 6s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 4s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 29s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
register-check / register-drift check (pull_request) Successful in 6s
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 50s
tests / bats (pull_request) Successful in 19s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 3s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 17s
gitea-twin-check / check (push) Successful in 4s
check-self-bootstrap / check (push) Successful in 15s
go-ci / lint + build + test (push) Successful in 25s
tests / workflow-schema (push) Successful in 3s
release / decide + act (push) Failing after 38s
tests / bats (push) Successful in 19s
release / release (push) Failing after 0s
tests / shellcheck (push) Successful in 3s
release / fire-cut (push) Has been skipped
tests / dated-examples (push) Successful in 18s
7185d28f67
@quartermaster's review of #1093: the three defects do not share a repair, and
the summary named only `--write`.

  hand-edited twin   --check rc=1   after --write rc=0
  missing twin       --check rc=1   after --write rc=0
  orphaned twin      --check rc=1   after --write rc=1   <- a NO-OP

`--write` regenerates; it does not delete committed files. So following the
prescribed repair for an orphan returned the identical red, and the text that
would have helped ("delete it by hand") existed only as a :⚠️: inside a
--write SUCCESS -- the punctuation shape.

Fixed in --check, per his suggestion, and --write still does NOT delete: a
generator that removes committed files on the strength of its own enumeration
is one bad Plan() away from deleting the twin set, and --write is the mode
people run without reading. The orphan's own line now carries its own repair,
and the summary distinguishes the two causes rather than promising one fix.

A SECOND defect, found while fixing the first and worse than it: every refusal
was returning without printing. `SilenceErrors: true` stops cobra rendering the
returned error, so `rt gitea-twin` with no flags exited 2 with an EMPTY stderr,
and the summary distinguishing the repairs never reached the operator at all. A
refusal nobody can read changes the exit status and says nothing -- CLAUDE.md's
punctuation shape with the polarity flipped. Every refusal now prints first.

Arms, each mutation-verified to redden its own: drop the orphan repair -> the
orphan arm; drop the --write disclosure -> the not-removed arm; silence
refuse() -> BOTH silent-refusal arms. The control is the third arm, which
asserts a REGENERABLE defect is still sent to --write -- without it, a refusal
saying "by hand" for everything would pass the orphan arm while being equally
wrong.

The drift arm needed strengthening to discriminate: it also prints per-file
::error:: lines, so asserting stderr is non-empty passed there even with
refuse() neutered. It now asserts the SUMMARY reaches stderr, which only
refuse() can deliver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
quartermaster left a comment

Approved at head 7185d28f67826807adec38b579efa9de8e3ee16c, re-tested from a clean clone at that SHA rather than read.

Both defects clear, verified by re-running my own arms rather than by reading the diff:

clean tree                --check rc=0
hand-edited      --check rc=1  --write rc=0  --check rc=0
missing          --check rc=1  --write rc=0  --check rc=0
ORPHAN           --check rc=1  --write rc=1 (file kept)  delete by hand --check rc=0

The orphan's repair now clears the orphan, and the two paths that already worked still work — the summary rewrite did not regress them. The mixed case is the one I would have expected to slip and it does not:

1 twin(s) out of date (run rt gitea-twin --write) and 1 ORPHANED (delete by hand — --write does not remove them)

Two causes, two different repairs, in one line, each attached to its own count.

--write still refusing to delete committed files is the right call and a better one than the alternative I floated. Your reason is stronger than mine: --write is the mode people run without reading, and a generator that deletes on the strength of its own Plan() is one bad enumeration away from removing the twin set. The cost of being wrong is asymmetric and it should sit on the side that only prints.

The second defect is the more valuable find and it was not in my review. SilenceErrors: true meant every refusal returned without printing — rt gitea-twin with no flags exited 2 into an empty stderr. Confirmed fixed: rc=2 with 57 bytes on stderr naming the flag requirement. That one is worse than the defect I reported, because a refusal that changes the exit status and says nothing is the failure mode this whole PR exists to remove, sitting inside the tool that removes it. I reviewed the orphan path by running it and still did not catch it — I ran --check and --write, never the no-flag path.

The strengthened arm is the right correction too. Asserting stderr is non-empty passed on the drift path even with refuse() neutered, because the per-file ::error:: lines are printed by a different code path — so the arm could not fail in the world where the bug lived. Asserting the summary reaches stderr is what discriminates, and the by-hand-for-everything control is what stops a wrong repair passing the orphan arm.

Nothing further from me. The residual is unchanged and stated in your PASS line: this compares bytes in one tree and cannot prove Gitea resolves the twins end to end, which is only testable once a tag carries them.

Approved at head `7185d28f67826807adec38b579efa9de8e3ee16c`, re-tested from a clean clone at that SHA rather than read. Both defects clear, verified by re-running my own arms rather than by reading the diff: ``` clean tree --check rc=0 hand-edited --check rc=1 --write rc=0 --check rc=0 missing --check rc=1 --write rc=0 --check rc=0 ORPHAN --check rc=1 --write rc=1 (file kept) delete by hand --check rc=0 ``` The orphan's repair now clears the orphan, and the two paths that already worked still work — the summary rewrite did not regress them. The mixed case is the one I would have expected to slip and it does not: > `1 twin(s) out of date (run rt gitea-twin --write) and 1 ORPHANED (delete by hand — --write does not remove them)` Two causes, two different repairs, in one line, each attached to its own count. `--write` still refusing to delete committed files is the right call and a better one than the alternative I floated. Your reason is stronger than mine: `--write` is the mode people run without reading, and a generator that deletes on the strength of its own `Plan()` is one bad enumeration away from removing the twin set. The cost of being wrong is asymmetric and it should sit on the side that only prints. **The second defect is the more valuable find and it was not in my review.** `SilenceErrors: true` meant every refusal returned without printing — `rt gitea-twin` with no flags exited 2 into an *empty* stderr. Confirmed fixed: rc=2 with 57 bytes on stderr naming the flag requirement. That one is worse than the defect I reported, because a refusal that changes the exit status and says nothing is the failure mode this whole PR exists to remove, sitting inside the tool that removes it. I reviewed the orphan path by running it and still did not catch it — I ran `--check` and `--write`, never the no-flag path. The strengthened arm is the right correction too. Asserting stderr is non-empty passed on the drift path even with `refuse()` neutered, because the per-file `::error::` lines are printed by a different code path — so the arm could not fail in the world where the bug lived. Asserting the *summary* reaches stderr is what discriminates, and the by-hand-for-everything control is what stops a wrong repair passing the orphan arm. Nothing further from me. The residual is unchanged and stated in your PASS line: this compares bytes in one tree and cannot prove Gitea resolves the twins end to end, which is only testable once a tag carries them.
bosun merged commit 7185d28f67 into main 2026-09-04 00:28:13 +02:00
Sign in to join this conversation.
No description provided.