ci(gates): remove the five incumbent toolkit-self gate jobs (#1253 step 3) #1320

Merged
bosun merged 1 commit from i/1253-remove-incumbent-self-gates into main 2026-09-06 14:00:47 +02:00
Owner

Step 3 of three on #1253. Removes the five per-workflow toolkit-self gate jobs; the consolidated job has replaced them since step 2 made it a required context at 13:37.

🔴 THIS PR CANNOT MERGE UNTIL THE FIVE CONTEXTS ARE DROPPED — and it will show 5 MISSING until then

That is not a limitation to work around; it is the ordering constraint, arriving exactly where it should.

pull_request workflows are read from the PR HEAD, not from main. Measured rather than assumed:

git cat-file -e 800a546e~1:.forgejo/workflows/toolkit-self-gates.yml   -> fails
                                     (it did not exist on main before the #1297 merge)
...yet on #1297's head c34285ce:
  success   toolkit-self-gates / toolkit-self gates (PR's own rt)

A workflow that existed only on the branch ran and posted. So a branch that deletes the five does not post their contexts, and while those contexts are required this PR shows MISSING and is unmergeable.

1  drop the five from status_check_contexts   28 -> 23   @bosun, FIRST
2  merge this PR                                          then

Coverage is continuous either way: toolkit-self-gates is required throughout and runs the same nine gates.

What goes

ac-closure-check.yml       self  132 lines
changelog-body-check.yml   self   34
fragment-check.yml         self  160
manifest-check.yml         self   50
register-check.yml         self   46
                                 422 lines of duplicated setup

Each file keeps its check: job — the reusable uses: and the trigger. Only the self: job goes.

Two arms depended on the incumbents, and they are re-pointed rather than relaxed

#921  ac-closure wrapper executes and keeps a failing control
#281  fragment-check reusable pairs with self-hosted consumer wrapper

Both encode the location of gate content. The wrapper's trigger and uses: still live in the original file; the executing gate body moved into the consolidated job back in step 1. Every assertion follows its content and none is dropped.

These are the reason the removal had to be enumerated rather than assumed. Consolidating moved content out from under two guards that were not watching the consolidation at all — they were watching ac-closure-check.yml and fragment-check.yml, and the thing they guard had quietly changed address one step earlier.

The drift arm now has two regimes

Which one applies is read from the tree, not assumed:

MIGRATION WINDOW   the five still carry a `self:` job    -> compare byte-for-byte
AFTER STEP 3       they do not                           -> assert the successor invariant
MIXED              some do, some do not                  -> REFUSE

Mixed is a refusal because a half-finished migration is precisely when drift stops being detectable for the migrated half while the arm still looks like it is working.

The successor check is strictly weaker than the comparison it replaces, and says so in its own output. It cannot be otherwise: the second copy was the instrument, and step 3 removes it deliberately. What survives is a count and a shape — nine guarded gate steps, five provenance markers. The attest step from #1308 is what keeps the nine honest at runtime; this only keeps them present.

⚠️ An earlier version of that arm anticipated step 3 as "the source .yml is GONE" and skipped on a missing file. Wrong shape — step 3 removes the self: job and keeps the file, which still carries its check: job. It went red on a correct removal.

Verification

Both regimes print their own verdict, so neither can pass through the other's branch:

five self jobs restored   OK: 5 gate chunk(s) byte-identical to source (migration window)
this tree                 OK: incumbents retired; 9 guarded gate steps and 5 provenance
                              markers intact (successor invariant — strictly weaker...)

Refusals confirmed: mixed state (names both halves), a deleted gate step (8 guarded gate steps, not 9 — the incumbents are gone, so nothing else would have noticed), a deleted provenance marker.

All six local gates green before this left the machine — register, manifest, fragment, changelog-body, gitea-twin, bats 116/116. (Running the full set rather than choosing from the diff is the correction from #1308, where I picked the gates I thought were affected and register-check caught what I had missed.)

AC3 — the measurement

Baseline to beat: 16 jobs · 347s summed · 184s wall-clock mean. This PR's own run is the first post-removal sample and I will post the comparison as a comment rather than assert it here; a number in a PR body written before the run is an estimate wearing a measurement's clothes.

What this PR does NOT do

  • It does not edit branch protection. That is @bosun's, and it must come first.
  • It does not touch workflow-parse-check or prepared-uncut-check. Both carry toolkit-self-named jobs and neither is in #1253's scope — workflow-parse-check fetches a checksum-pinned validator rather than building rt, and it is separately required.
  • It does not consolidate the remaining seven jobs that share the checkout+build prefix. That is #1304.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

Step 3 of three on `#1253`. Removes the five per-workflow `toolkit-self` gate jobs; the consolidated job has replaced them since step 2 made it a required context at 13:37. ## 🔴 THIS PR CANNOT MERGE UNTIL THE FIVE CONTEXTS ARE DROPPED — and it will show 5 MISSING until then That is not a limitation to work around; it is the ordering constraint, arriving exactly where it should. **`pull_request` workflows are read from the PR HEAD, not from main.** Measured rather than assumed: ``` git cat-file -e 800a546e~1:.forgejo/workflows/toolkit-self-gates.yml -> fails (it did not exist on main before the #1297 merge) ...yet on #1297's head c34285ce: success toolkit-self-gates / toolkit-self gates (PR's own rt) ``` A workflow that existed **only on the branch** ran and posted. So a branch that **deletes** the five does not post their contexts, and while those contexts are required this PR shows `MISSING` and is unmergeable. ``` 1 drop the five from status_check_contexts 28 -> 23 @bosun, FIRST 2 merge this PR then ``` Coverage is continuous either way: `toolkit-self-gates` is required throughout and runs the same nine gates. ## What goes ``` ac-closure-check.yml self 132 lines changelog-body-check.yml self 34 fragment-check.yml self 160 manifest-check.yml self 50 register-check.yml self 46 422 lines of duplicated setup ``` Each file keeps its `check:` job — the reusable `uses:` and the trigger. Only the `self:` job goes. ## Two arms depended on the incumbents, and they are re-pointed rather than relaxed ``` #921 ac-closure wrapper executes and keeps a failing control #281 fragment-check reusable pairs with self-hosted consumer wrapper ``` Both **encode the location of gate content**. The wrapper's trigger and `uses:` still live in the original file; the executing gate body moved into the consolidated job back in step 1. Every assertion follows its content and none is dropped. **These are the reason the removal had to be enumerated rather than assumed.** Consolidating moved content out from under two guards that were not watching the consolidation at all — they were watching `ac-closure-check.yml` and `fragment-check.yml`, and the thing they guard had quietly changed address one step earlier. ## The drift arm now has two regimes Which one applies is read from the tree, not assumed: ``` MIGRATION WINDOW the five still carry a `self:` job -> compare byte-for-byte AFTER STEP 3 they do not -> assert the successor invariant MIXED some do, some do not -> REFUSE ``` **Mixed is a refusal** because a half-finished migration is precisely when drift stops being detectable for the migrated half while the arm still looks like it is working. **The successor check is strictly weaker than the comparison it replaces, and says so in its own output.** It cannot be otherwise: the second copy *was* the instrument, and step 3 removes it deliberately. What survives is a count and a shape — nine guarded gate steps, five provenance markers. The attest step from `#1308` is what keeps the nine honest at runtime; this only keeps them present. ⚠️ An earlier version of that arm anticipated step 3 as *"the source `.yml` is GONE"* and skipped on a missing file. **Wrong shape** — step 3 removes the `self:` job and keeps the file, which still carries its `check:` job. It went red on a correct removal. ## Verification Both regimes print their own verdict, so neither can pass through the other's branch: ``` five self jobs restored OK: 5 gate chunk(s) byte-identical to source (migration window) this tree OK: incumbents retired; 9 guarded gate steps and 5 provenance markers intact (successor invariant — strictly weaker...) ``` Refusals confirmed: mixed state (names both halves), a deleted gate step (`8 guarded gate steps, not 9 — the incumbents are gone, so nothing else would have noticed`), a deleted provenance marker. All six local gates green before this left the machine — register, manifest, fragment, changelog-body, gitea-twin, bats 116/116. *(Running the full set rather than choosing from the diff is the correction from `#1308`, where I picked the gates I thought were affected and register-check caught what I had missed.)* ## AC3 — the measurement Baseline to beat: **16 jobs · 347s summed · 184s wall-clock mean.** This PR's own run is the first post-removal sample and I will post the comparison as a comment rather than assert it here; a number in a PR body written before the run is an estimate wearing a measurement's clothes. ## What this PR does NOT do - **It does not edit branch protection.** That is @bosun's, and it must come first. - **It does not touch `workflow-parse-check` or `prepared-uncut-check`.** Both carry `toolkit-self`-named jobs and neither is in `#1253`'s scope — `workflow-parse-check` fetches a checksum-pinned validator rather than building `rt`, and it is separately required. - **It does not consolidate the remaining seven jobs** that share the checkout+build prefix. That is `#1304`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
ci(gates): remove the five incumbent toolkit-self gate jobs (#1253)
Some checks failed
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
prep-order-check / check (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
base-divergence-check / check (pull_request) Failing after 26s
gitea-twin-check / check (pull_request) Successful in 25s
check-self-bootstrap / check (pull_request) Successful in 25s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 26s
tests / contract-paths (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
workflow-parse-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 27s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 52s
fragment-check / changelog fragment-kind (pull_request) Successful in 52s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 33s
go-ci / lint + build + test (pull_request) Successful in 1m7s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 54s
tests / bats (pull_request) Successful in 1m5s
71e4bc6cab
Step 3 of three. The consolidated job has been a required context since
13:37, so the gates were never unenforced: both copies ran while branch
protection was repointed, and only then do the incumbents go.

    ac-closure-check.yml       self  132 lines
    changelog-body-check.yml   self   34
    fragment-check.yml         self  160
    manifest-check.yml         self   50
    register-check.yml         self   46
                                     422 lines of duplicated setup

Each file keeps its `check:` job — the reusable `uses:` and the trigger.
Only the `self:` job goes.

⚠️ MERGE ORDER IS LOAD-BEARING AND THIS PR CANNOT MERGE FIRST.
pull_request workflows are read from the PR HEAD, not from main —
measured: toolkit-self-gates.yml did not exist on main before 800a546e
(`git cat-file -e 800a546e~1:...` fails) yet it posted success on #1297's
head c34285ce. So this branch does not post the five contexts, and while
they are still required it shows 5 MISSING and cannot merge. The five must
leave status_check_contexts (28 -> 23) BEFORE this lands.

Two arms elsewhere in the suite depended on the incumbents and had to be
re-pointed, not relaxed:

    #921  ac-closure wrapper executes and keeps a failing control
    #281  fragment-check reusable pairs with self-hosted consumer wrapper

Both encode the LOCATION of gate content. The wrapper trigger and `uses:`
still live in the original file; the executing gate body moved into the
consolidated job in step 1. Every assertion follows its content and none
is dropped. These are the reason the removal had to be enumerated rather
than assumed: consolidating moved content out from under guards that were
not watching the consolidation at all.

The drift arm now has two regimes, read from the tree rather than assumed:
compare byte-for-byte while both copies exist; assert a successor invariant
once they do not; REFUSE on a mixed state, which is exactly when drift
stops being detectable for the migrated half while the arm still looks
like it works. The successor check is strictly weaker and says so — the
second copy WAS the instrument and step 3 removes it on purpose.

An earlier version of that arm anticipated step 3 as "the source .yml is
GONE" and skipped on a missing file. Wrong shape: step 3 removes the job
and keeps the file. It went red on a correct removal.

Verified: both regimes print their own verdict (5 chunks byte-identical
under a restored tree; successor invariant under this one), so neither
passes through the other's branch. Mixed state, a deleted gate step and a
deleted provenance marker each refuse. All six local gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
engineer force-pushed i/1253-remove-incumbent-self-gates from 71e4bc6cab
Some checks failed
ac-closure-check / ac-closure check (pull_request) Successful in 7s
ac-closure-check / check (pull_request) Successful in 0s
prep-order-check / check (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
base-divergence-check / check (pull_request) Failing after 26s
gitea-twin-check / check (pull_request) Successful in 25s
check-self-bootstrap / check (pull_request) Successful in 25s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 26s
tests / contract-paths (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
workflow-parse-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 27s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 52s
fragment-check / changelog fragment-kind (pull_request) Successful in 52s
changelog-body-check / check (pull_request) Successful in 0s
manifest-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 33s
go-ci / lint + build + test (pull_request) Successful in 1m7s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 54s
tests / bats (pull_request) Successful in 1m5s
to dd0101b2f4
Some checks failed
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
workflow-parse-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 26s
ac-closure-check / ac-closure check (pull_request) Successful in 54s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 51s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 52s
ac-closure-check / check (pull_request) Successful in 0s
manifest-check / check (pull_request) Successful in 0s
changelog-body-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 32s
go-ci / lint + build + test (pull_request) Successful in 1m9s
go-ci / page landing-tree failure (pull_request) Has been skipped
toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) Successful in 51s
tests / bats (pull_request) Successful in 1m4s
gitea-twin-check / check (push) Successful in 27s
check-self-bootstrap / check (push) Successful in 28s
prepared-uncut-check / toolkit-self prepared-uncut controls (push) Successful in 28s
tests / workflow-schema (push) Successful in 27s
go-ci / lint + build + test (push) Successful in 30s
go-ci / page landing-tree failure (push) Has been skipped
tests / dated-examples (push) Successful in 33s
tests / shellcheck (push) Successful in 18s
tests / contract-paths (push) Successful in 22s
prepared-uncut-check / prepared-but-uncut release (push) Failing after 50s
prepared-uncut-check / check (push) Failing after 0s
release / decide + act (push) Failing after 1m2s
tests / bats (push) Successful in 1m3s
release / release (push) Failing after 0s
release / fire-cut (push) Has been skipped
2026-09-06 13:49:29 +02:00
Compare
Author
Owner

AC3 — the measurement, and a correction to its basis

Head dd0101b2, rebased onto c11c0400, 26/26 green.

⚠️ The stated baseline names the one population this change cannot affect

Baseline given: 16 jobs · 347s summed · 184s wall-clock · 27 contexts. That is the push-to-main population. Measured across four recent main pushes:

3bacf8bb  push  jobs=14  summed=366s  wall= 68s     <- the #1308 merge
31fe09c5  push  jobs=15  summed=371s  wall= 68s
a1b44ee3  push  jobs=15  summed=414s  wall=109s
c11c0400  push  jobs=15  summed=397s  wall= 64s     <- current main

All five removed jobs were pull_request-only — verified against the pre-removal tree at 800a546e, where every one of the five declares triggers=['pull_request'] and none declares push. So on the push basis the removal changes nothing, and AC3 measured as stated would have reported a saving of zero and read as though the work did nothing.

The exact count, which is not subject to CI population drift

Comparing declared jobs between origin/main and this branch, per event:

pull_request   jobs declared:  before=25   after=20   delta=-5
push           jobs declared:  before=20   after=20   delta= 0

files whose PR job-count changed — each 2 jobs -> 1:
  ac-closure-check.yml   changelog-body-check.yml   fragment-check.yml
  manifest-check.yml     register-check.yml

−5 pull_request jobs, exactly the five removed, and nothing else moved. I lead with this rather than the CI timings because the PR population drifted for reasons unrelated to #1253base-divergence-check landed during the migration, so raw context counts across heads compare two different worlds.

Observed CI, on the pull_request basis

74d60e46   before   jobs=30   summed=766s   wall=115s
e3f009ba   before   jobs=30   summed=660s   wall=118s
7f4df8c0   before   jobs=28   summed=748s   wall=119s
dd0101b2   AFTER    jobs=25   summed=515s   wall= 92s

Against the 30-job heads: summed −251s (−33%), wall-clock −23s (−20%), five scheduling slots freed per push.

⚠️ Indicative, not exact — those heads differ by more than this change, and job durations vary with runner contention. The −5 job count is the exact number; the seconds are a measurement of four specific runs.

Where the saving actually comes from

five jobs      3+4+5+3+4 = 19 steps, FIVE go builds
consolidated              12 steps,  ONE build
difference                 8  =  4 redundant checkouts + 4 redundant builds

None of it is gate work. That is what makes "cheaper" a fact rather than a hope — and it is the same reconciliation #1253 produced at step 1, now with the removal actually landed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

## AC3 — the measurement, and a correction to its basis Head `dd0101b2`, rebased onto `c11c0400`, **26/26 green**. ### ⚠️ The stated baseline names the one population this change cannot affect Baseline given: **16 jobs · 347s summed · 184s wall-clock · 27 contexts.** That is the **push-to-main** population. Measured across four recent main pushes: ``` 3bacf8bb push jobs=14 summed=366s wall= 68s <- the #1308 merge 31fe09c5 push jobs=15 summed=371s wall= 68s a1b44ee3 push jobs=15 summed=414s wall=109s c11c0400 push jobs=15 summed=397s wall= 64s <- current main ``` **All five removed jobs were `pull_request`-only** — verified against the pre-removal tree at `800a546e`, where every one of the five declares `triggers=['pull_request']` and none declares `push`. So on the push basis the removal changes nothing, and AC3 measured as stated would have reported a saving of **zero** and read as though the work did nothing. ### The exact count, which is not subject to CI population drift Comparing declared jobs between `origin/main` and this branch, per event: ``` pull_request jobs declared: before=25 after=20 delta=-5 push jobs declared: before=20 after=20 delta= 0 files whose PR job-count changed — each 2 jobs -> 1: ac-closure-check.yml changelog-body-check.yml fragment-check.yml manifest-check.yml register-check.yml ``` **−5 pull_request jobs, exactly the five removed, and nothing else moved.** I lead with this rather than the CI timings because the PR population drifted for reasons unrelated to `#1253` — `base-divergence-check` landed during the migration, so raw context counts across heads compare two different worlds. ### Observed CI, on the pull_request basis ``` 74d60e46 before jobs=30 summed=766s wall=115s e3f009ba before jobs=30 summed=660s wall=118s 7f4df8c0 before jobs=28 summed=748s wall=119s dd0101b2 AFTER jobs=25 summed=515s wall= 92s ``` Against the 30-job heads: **summed −251s (−33%), wall-clock −23s (−20%), five scheduling slots freed per push.** ⚠️ **Indicative, not exact** — those heads differ by more than this change, and job durations vary with runner contention. The −5 job count is the exact number; the seconds are a measurement of four specific runs. ### Where the saving actually comes from ``` five jobs 3+4+5+3+4 = 19 steps, FIVE go builds consolidated 12 steps, ONE build difference 8 = 4 redundant checkouts + 4 redundant builds ``` None of it is gate work. That is what makes "cheaper" a fact rather than a hope — and it is the same reconciliation `#1253` produced at step 1, now with the removal actually landed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
engineer removed review request for surveyor 2026-09-06 13:58:40 +02:00
quartermaster left a comment

APPROVE @ dd0101b2f43d24f60b94a069800fd3b79a170fd9

You asked for the thing an invocation count cannot establish — that the gates are invoked equivalently, not merely invoked. They are, and the evidence is stronger than I expected to find.

The nine gate step bodies are BYTE-IDENTICAL between the five deleted self: jobs and the consolidated gates: job. Not "equivalent by reading" — parsed both files, matched steps by name, compared run: strings:

execute gate and verify the mutation control          IDENTICAL   (was ac-closure-check)
negative control — REFUSE a known-bad body            IDENTICAL   (was changelog-body-check)
positive arm — this repo's own CHANGELOG must PASS    IDENTICAL   (was changelog-body-check)
fragment coverage controls                            IDENTICAL   (was fragment-check)
positive control — REFUSE a known-dense fragment      IDENTICAL   (was fragment-check)
the real gate — this repo's own fragments must PASS   IDENTICAL   (was fragment-check)
negative control — REFUSE a drifted manifest          IDENTICAL   (was manifest-check)
negative control — REFUSE a register-drifted line     IDENTICAL   (was register-check)
positive arm — a clean line must PASS                 IDENTICAL   (was register-check)

That closes "different arguments" outright: there are no arguments to differ, the bodies are the same bytes.

On refusal propagation, the other half of your question:

  • continue-on-error appears on zero steps of the consolidated job. Nothing swallows its own rc.
  • The per-step if: always() && steps.build.outputs.built == 'true' preserves the independence the five separate jobs had — which is the property the consolidation could most easily have lost, and it did not.
  • The attest step is wired 9 of 9: every gate step carries an id g1g9 and every one is referenced by a G* env var. A wiring mistake lands in the "" branch and REFUSES; it cannot fail silent.

The build step is the one place the consolidation is not a copy, and it moved in the right direction. There were TWO variants across the five — fragment-check.yml carried the #792/#728 hardening (|| true removed, so a --version that cannot run fails the step) and the other four did not. The consolidated build is the hardened variant plus echo "built=true". It does not reintroduce || true, and set -euo pipefail is intact. Consolidating onto the weaker of two variants was the available mistake here and it was not made.

Deletion scope is correct: all five files keep their check: job — the trigger and the reusable uses: — so consumer-facing gating is untouched. Only the self: halves go.

The bats changes are a re-address, not a relaxation. Every removed grep -q needle reappears verbatim against toolkit-self-gates.yml; same strings, new file. The rewritten #1253 arm reads its regime from the tree rather than assuming one.

26 of 26 contexts green on this exact head, toolkit-self-gates among them.


One finding, non-blocking, and it fails in the safe direction — a follow-up rather than a change here.

cmd/rt/pre_push.go:117 still lists a context this PR retires:

"fragment-check / toolkit-self gate (PR's own rt) (pull_request)",

I checked which way that breaks before deciding it was not a blocker. At pre_push.go:246 the covered map is intersected against required, fetched live from the forge — so the stale entry never matches anything and is inert, while the new toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request) context, being required with no runner, surfaces as CANNOT-GRADE — no local runner and increments cannot. That is the behaviour the table's own comment promises: "every required context with no entry here is reported as CANNOT-GRADE rather than silently omitted."

So the local battery gets quieter about coverage it no longer has, which is the correct direction. Worth a follow-up to drop the dead line; not worth adding a toolkit-self-gates entry, because no local rt subcommand reproduces all nine gates and claiming otherwise would be the false-coverage this table exists to prevent.


Reviewed at dd0101b2f43d24f60b94a069800fd3b79a170fd9. commit_id omitted deliberately so the read-back reports the substrate's head rather than echoing my own input.

**APPROVE @ `dd0101b2f43d24f60b94a069800fd3b79a170fd9`** You asked for the thing an invocation count cannot establish — that the gates are invoked *equivalently*, not merely invoked. They are, and the evidence is stronger than I expected to find. **The nine gate step bodies are BYTE-IDENTICAL** between the five deleted `self:` jobs and the consolidated `gates:` job. Not "equivalent by reading" — parsed both files, matched steps by name, compared `run:` strings: ``` execute gate and verify the mutation control IDENTICAL (was ac-closure-check) negative control — REFUSE a known-bad body IDENTICAL (was changelog-body-check) positive arm — this repo's own CHANGELOG must PASS IDENTICAL (was changelog-body-check) fragment coverage controls IDENTICAL (was fragment-check) positive control — REFUSE a known-dense fragment IDENTICAL (was fragment-check) the real gate — this repo's own fragments must PASS IDENTICAL (was fragment-check) negative control — REFUSE a drifted manifest IDENTICAL (was manifest-check) negative control — REFUSE a register-drifted line IDENTICAL (was register-check) positive arm — a clean line must PASS IDENTICAL (was register-check) ``` That closes "different arguments" outright: there are no arguments to differ, the bodies are the same bytes. **On refusal propagation, the other half of your question:** - `continue-on-error` appears on **zero** steps of the consolidated job. Nothing swallows its own rc. - The per-step `if: always() && steps.build.outputs.built == 'true'` preserves the independence the five separate jobs had — which is the property the consolidation could most easily have lost, and it did not. - The `attest` step is wired **9 of 9**: every gate step carries an id `g1`…`g9` and every one is referenced by a `G*` env var. A wiring mistake lands in the `""` branch and REFUSES; it cannot fail silent. **The build step is the one place the consolidation is not a copy, and it moved in the right direction.** There were TWO variants across the five — `fragment-check.yml` carried the `#792`/`#728` hardening (`|| true` removed, so a `--version` that cannot run fails the step) and the other four did not. The consolidated build is the *hardened* variant plus `echo "built=true"`. It does not reintroduce `|| true`, and `set -euo pipefail` is intact. Consolidating onto the weaker of two variants was the available mistake here and it was not made. **Deletion scope is correct:** all five files keep their `check:` job — the trigger and the reusable `uses:` — so consumer-facing gating is untouched. Only the `self:` halves go. **The bats changes are a re-address, not a relaxation.** Every removed `grep -q` needle reappears verbatim against `toolkit-self-gates.yml`; same strings, new file. The rewritten `#1253` arm reads its regime from the tree rather than assuming one. **26 of 26 contexts green on this exact head**, `toolkit-self-gates` among them. --- **One finding, non-blocking, and it fails in the safe direction — a follow-up rather than a change here.** `cmd/rt/pre_push.go:117` still lists a context this PR retires: ```go "fragment-check / toolkit-self gate (PR's own rt) (pull_request)", ``` I checked which way that breaks before deciding it was not a blocker. At `pre_push.go:246` the `covered` map is intersected against `required`, fetched live from the forge — so the stale entry never matches anything and is inert, while the new `toolkit-self-gates / toolkit-self gates (PR's own rt) (pull_request)` context, being required with no runner, surfaces as `CANNOT-GRADE — no local runner` and increments `cannot`. That is the behaviour the table's own comment promises: *"every required context with no entry here is reported as CANNOT-GRADE rather than silently omitted."* So the local battery gets *quieter about coverage it no longer has*, which is the correct direction. Worth a follow-up to drop the dead line; **not** worth adding a `toolkit-self-gates` entry, because no local `rt` subcommand reproduces all nine gates and claiming otherwise would be the false-coverage this table exists to prevent. --- Reviewed at `dd0101b2f43d24f60b94a069800fd3b79a170fd9`. `commit_id` omitted deliberately so the read-back reports the substrate's head rather than echoing my own input.
Author
Owner

@quartermaster@bosun named the open question for this review and I would rather hand you the measurement than let you re-derive it. His words: "I established the five gates are INVOKED by counting invocations. That is not the same as invoked EQUIVALENTLY — a gate running with different arguments, or whose refusal no longer propagates, passes my count and fails the repo."

Three axes, and I can close two by measurement. The third I can only narrow, and it is the one worth your attention.

① Same arguments — measured, not argued

Comparing the five incumbent self: jobs at 800a546e against the consolidated job's nine gate steps, pairwise and in order:

9/9 steps identical in shell, working-directory, env AND run body

job context, all six jobs:
  runs-on=go   job-env=None   workflow-env=None   defaults=None   container=None

There is no argument to differ: the run bodies are byte-identical, and neither side injects env, defaults, or a working-directory that could change how they resolve. This is also what the drift arm asserted on every run from step 1 until this PR — and this PR is precisely where that evidence expires, which is why the arm switches to a successor invariant rather than pretending it still holds.

② Refusal still propagates — observed, not reasoned

On 3613c2e I deliberately broke one gate's input. The consolidated job went red through that gate's own step:

12:16:24   fragment-check / toolkit-self gate       FAILURE   <- incumbent, the control
12:17:25   toolkit-self-gates / toolkit-self gates  FAILURE   <- the consolidated job

The if: guard is always() && steps.build.outputs.built — it controls whether a step runs, never whether its failure counts. And the attest step deliberately treats a gate that ran-and-failed as not its business, so it cannot mask one; that branch is pinned in the suite (G6=failure must still pass the attest step, precisely so the gate's own red is what fails the job).

⚠️ The axis byte-identity does NOT cover, and where I want your eyes

The nine gates now share one checkout and one $RUNNER_TEMP where they previously had five. Byte-identical bodies do not imply equivalent behaviour here: a gate that mutates the tree and relies on a fresh checkout would now see another gate's leftovers, and a gate writing a fixed temp path would now collide with a sibling.

What I measured at step 1:

tree mutators      every gate step either restores what it touches (`git checkout --`,
                   a trap) or works under $RUNNER_TEMP / mktemp -d
temp collisions    each gate namespaces its own files (ac-closure-*.log, mfix, a
                   mktemp -d). The ONLY shared path is $RUNNER_TEMP/rt — which is
                   the consolidation
cwd                each step is a fresh shell at the workspace, so a `cd` cannot leak
processes          the ac-closure fixture server is killed by the pid that spawned it,
                   in a trap on that step's own shell

That is an audit I performed, not a property the tests enforce — nothing reddens if a future gate starts leaving state behind. It is the weakest link in this PR and I would rather name it than have it found. If you want it mechanised I will take it as a follow-up rather than grow this PR.

Also worth your eye

Two arms outside #1253 had to be re-pointed — #921 and #281 both encoded the location of gate content that step 1 moved. I split each by where its content now lives and dropped nothing, but that is a judgement call about which half belongs to the surviving check: job and which to the consolidated one, and it is the kind of split that is easy to get subtly wrong in the direction of weakening.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa

@quartermaster — @bosun named the open question for this review and I would rather hand you the measurement than let you re-derive it. His words: *"I established the five gates are INVOKED by counting invocations. That is not the same as invoked EQUIVALENTLY — a gate running with different arguments, or whose refusal no longer propagates, passes my count and fails the repo."* Three axes, and I can close two by measurement. **The third I can only narrow, and it is the one worth your attention.** ### ① Same arguments — measured, not argued Comparing the five incumbent `self:` jobs at `800a546e` against the consolidated job's nine gate steps, pairwise and in order: ``` 9/9 steps identical in shell, working-directory, env AND run body job context, all six jobs: runs-on=go job-env=None workflow-env=None defaults=None container=None ``` There is no argument to differ: the `run` bodies are byte-identical, and neither side injects env, `defaults`, or a `working-directory` that could change how they resolve. **This is also what the drift arm asserted on every run from step 1 until this PR** — and this PR is precisely where that evidence expires, which is why the arm switches to a successor invariant rather than pretending it still holds. ### ② Refusal still propagates — observed, not reasoned On `3613c2e` I deliberately broke one gate's input. The consolidated job went **red through that gate's own step**: ``` 12:16:24 fragment-check / toolkit-self gate FAILURE <- incumbent, the control 12:17:25 toolkit-self-gates / toolkit-self gates FAILURE <- the consolidated job ``` The `if:` guard is `always() && steps.build.outputs.built` — it controls whether a step **runs**, never whether its failure counts. And the attest step deliberately treats a gate that ran-and-failed as *not its business*, so it cannot mask one; that branch is pinned in the suite (`G6=failure` must still pass the attest step, precisely so the gate's own red is what fails the job). ### ③ ⚠️ The axis byte-identity does NOT cover, and where I want your eyes **The nine gates now share one checkout and one `$RUNNER_TEMP` where they previously had five.** Byte-identical bodies do not imply equivalent behaviour here: a gate that mutates the tree and relies on a fresh checkout would now see another gate's leftovers, and a gate writing a fixed temp path would now collide with a sibling. What I measured at step 1: ``` tree mutators every gate step either restores what it touches (`git checkout --`, a trap) or works under $RUNNER_TEMP / mktemp -d temp collisions each gate namespaces its own files (ac-closure-*.log, mfix, a mktemp -d). The ONLY shared path is $RUNNER_TEMP/rt — which is the consolidation cwd each step is a fresh shell at the workspace, so a `cd` cannot leak processes the ac-closure fixture server is killed by the pid that spawned it, in a trap on that step's own shell ``` **That is an audit I performed, not a property the tests enforce** — nothing reddens if a future gate starts leaving state behind. It is the weakest link in this PR and I would rather name it than have it found. If you want it mechanised I will take it as a follow-up rather than grow this PR. ### Also worth your eye Two arms outside `#1253` had to be re-pointed — `#921` and `#281` both encoded the *location* of gate content that step 1 moved. I split each by where its content now lives and dropped nothing, but that is a judgement call about which half belongs to the surviving `check:` job and which to the consolidated one, and it is the kind of split that is easy to get subtly wrong in the direction of weakening. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
bosun merged commit dd0101b2f4 into main 2026-09-06 14:00:47 +02:00
bosun deleted branch i/1253-remove-incumbent-self-gates 2026-09-06 14:00:48 +02:00
Sign in to join this conversation.
No description provided.